在 Java 中将 Ed25519 公钥编码为 SSH 格式

Encoding a Ed25519 Public Key to SSH format in Java

提问人:Atanas Donev 提问时间:11/10/2023 更新时间:11/11/2023 访问量:67

问:

首先,我是密码学和不同类型的密钥/编码/格式的新手,所以如果我在某个地方错了,请纠正我。 我有一个 java 应用程序,需要将 Ed25519 密钥保存到密钥库。该应用程序是旧版应用程序,某些方法和库无法更改。它使用 Apache MINA SSHD 和 BouncyCastle 将密钥存储在密钥库中,并将公钥编码为 SSH 格式。RSA 和 DSA 密钥没有问题。问题在于 Apache MINA 使用 EdDSA 密钥的 net.i2p 实现,而 BouncyCastle 使用自己的 BCEdDSA 密钥。我们使用的方法来自 MINA 并返回 witch 的 net.i2p 密钥 我无法将其保存到密钥库,因为 BouncyCastle 的 JCAContentSigner 无法识别密钥的这种实现。在对公钥进行编码时,问题就出现了。

我创建了一个实现 ContentSigner 的类,并使用 net.i2p 的 EdDSAEngine 类来创建签名者。

private class EdDSAContentSigner implements ContentSigner
    {
        private final AlgorithmIdentifier sigAlgId;
        private final PrivateKey privateKey;
        private ByteArrayOutputStream stream;

        public EdDSAContentSigner(AlgorithmIdentifier sigAlgId, PrivateKey privKey)
        {
            this.sigAlgId = sigAlgId;
            this.privateKey = privKey;
            this.stream = new ByteArrayOutputStream();
        }


        @Override
        public AlgorithmIdentifier getAlgorithmIdentifier()
        {
            return sigAlgId;
        }


        @Override
        public OutputStream getOutputStream()
        {
            stream.reset();
            return stream;
        }


        @Override
        public byte[] getSignature()
        {
            byte[] dataToSign = stream.toByteArray();
            try
            {
                EdDSAEngine sig = new EdDSAEngine();
                sig.initSign(privateKey);
                return sig.signOneShot(dataToSign);
            }
            catch (GeneralSecurityException e)
            {
                LOG.error("Cannot sign data : " + e.getMessage(), e);
                throw new IllegalStateException("Cannot sign data : " + e.getMessage(), e);
            }
        }
    }

然后我设法创建了一个证书并将密钥保存到密钥库。 该应用程序具有上传私钥文件的功能,它以 SSH 格式生成公钥 - 从 ssh-ed25519 开始。

现在我在将公钥编码为 SSH 格式时遇到了问题。即使我使用相同的私钥文件,它也总是与 ssh-keygen 工具生成的不同。同样,公钥可以是不同类型的 net.i2p/BouncyCastle/sun.security.ec.ed.EdDSAPublicKeyImpl,具体取决于调用我的 encodeEdDSAPublicKey 方法的方法。我尝试了不同的方法来编码它,从在 PublicKey 本身上调用 .getEncoded() 方法到使用 BouncyCastle 的 ASN1InputStream,这是我的最新方法。我还将提供 RSA 密钥编码方法 witch 返回与 ssh-keygen 工具相同的编码。我希望能够使用公钥通过 putty 连接到服务器。任何帮助/建议将不胜感激。

    private static String encodeEdDSAPublicKey(PublicKey publicKey)
        throws IOException
    {
       try(ASN1InputStream asn1InputStream = new ASN1InputStream(publicKey.getEncoded()))
        {
            ASN1Primitive primitive = asn1InputStream.readObject();
            byte[] keyBytes ((ASN1Sequence)primitive).getObjectAt(1).toASN1Primitive().getEncoded();
          ByteArrayOutputStream byteOs = new ByteArrayOutputStream();
          DataOutputStream dos = new DataOutputStream(byteOs);
          dos.writeInt("ssh-ed25519".getBytes().length);
          dos.write("ssh-ed25519".getBytes());
          dos.writeInt(keyBytes.length);
          dos.write(keyBytes);
          return Base64.getEncoder().encodeToString(byteOs.toByteArray());
        }
    }
    private static String encodeRSAPublicKey(PublicKey publicKey)
        throws IOException
    {
        String publicKeyEncoded;
        RSAPublicKey rsaPublicKey = (RSAPublicKey)publicKey;
        ByteArrayOutputStream byteOs = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(byteOs);
        dos.writeInt("ssh-rsa".getBytes().length);
        dos.write("ssh-rsa".getBytes());
        dos.writeInt(rsaPublicKey.getPublicExponent().toByteArray().length);
        dos.write(rsaPublicKey.getPublicExponent().toByteArray());
        dos.writeInt(rsaPublicKey.getModulus().toByteArray().length);
        dos.write(rsaPublicKey.getModulus().toByteArray());
        publicKeyEncoded = Base64.getEncoder().encodeToString(byteOs.toByteArray());
        return publicKeyEncoded;
    }

这是我根据算法选择正确编码方法的方法

    public static String encodePublicKey(PublicKey publicKey, String name)
        throws IOException
    {
        String suffix = "";
        String algorithm = publicKey.getAlgorithm();

        if (name != null)
        {
            suffix = name);
        }

        switch (algorithm)
        {
        case "RSA":
            return "ssh-rsa " + encodeRSAPublicKey(publicKey) + suffix;
        case "DSA":
            return "ssh-dss " + encodeDSAPublicKey(publicKey) + suffix;
        case "Ed25519":
        case "EdDSA":
            return "ssh-ed25519 " + encodeEdDSAPublicKey(publicKey) + suffix;
        default:
            throw new IOException("Unknown public key encoding: " + publicKey.getAlgorithm());
        }
    }
java bouncycastle apache-mina ed25519 eddsa

评论


答:

0赞 dave_thompson_085 11/11/2023 #1

你离得很近。Java 是一个 SPKI 结构,其字段 1 是一个 BIT STRING,其中包含特定于算法的数据,对于 EdDSA 来说,这些数据是原始点编码。用:PublicKey.getEncoded()

    ASN1Object spki = new ASN1InputStream(pubkey.getEncoded()) .readObject();
    // or wrapped in the try as you have it is slightly cleaner
    byte[] point = ((ASN1BitString) ((ASN1Sequence)spki).getObjectAt(1) ).getOctets();

或者使用特定于类型的 Bouncy 类来为您处理解析和类型转换:

    byte[] point = SubjectPublicKeyInfo.getInstance(pubkey.getEncoded()).getPublicKeyData().getOctets();

或者,如果使用 Bouncy 提供程序(和键类),更简单地使用:

    byte[] point = ((org.bouncycastle.jcajce.interfaces.EdDSAPublicKey)pubkey).getPointEncoding();
    // or org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey 

无论如何,将长度和内容写入SSH格式,就像您已经拥有的那样。point