[memo] Generate RSA key pair for SSH in Java

Introduction

By the way, if you normally use ssh-keygen, it looks like the following.

ssh-keygen -t rsa -b 4096

RSA Review "RSA Key Pair Overview"

I will borrow the explanation from the following Qiita article.

(Borrowed from here)

If the two large prime numbers are $ p $ and $ q $,

N = pq \tag{1}

Let $ L $ be the variable that satisfies (2) below.

L = lcm(p-1, q-1) \tag{2}

Furthermore, let $ E $ be a variable that satisfies the following equation (3).

gcd(E, L) = 1 \tag{3}

Finally, let $ D $ be the variable that satisfies equation (4).

ED≡1 (mod L) \tag{4}

The $ (E, N) $ pair obtained so far is the * public key *, and the $ (D, N) $ pair is the * private key *.

(Borrowed so far)

OpenSSH private key file format

As I wrote at the beginning, I assume PEM format and no passphrase.

It's basically like PKCS # 8 in PEM format.

For details, see "PKCS # 8 format" in "RSA public key file format and fingerprint" above.

By the way, for Java's RSAPrivateKey instance, you should be able to get the binary data with the getEncoded () method, so you should do MIME-like Base64 encoding.

    public Optional<String> encodePrivateKey(final PrivateKey privateKey) {

        final String keyType;
        if (privateKey instanceof RSAPrivateKey) {
            keyType = "RSA";
        } else if (privateKey instanceof DSAPrivateKey) {
            keyType = "DSA";
        } else if (privateKey instanceof ECPrivateKey) {
            keyType = "EC";
        } else {
            return Optional.empty();
        }

        final byte[] privateBytes = privateKey.getEncoded();
        final String privateBase64 = Base64.getMimeEncoder()
                .encodeToString(privateBytes);

        final String encodedPrivateKey = "-----BEGIN " + keyType + " PRIVATE KEY-----\n"
                + privateBase64 + "\n"
                + "-----END " + keyType + " PRIVATE KEY-----";
        return Optional.of(encodedPrivateKey);
    }

OpenSSH RSA public key file format

Unlike the format described in RFC4716, in the case of OpenSSH format, there is no "BEGIN" / "END" delimiter before and after, but at the beginning. It has the signature "ssh-rsa".

Subsequent data is similar to the RFC4716 format (from the RSA Public Key File Format and Fingerprint article above).

Details of this format can be found in "6.6. Public Key Algorithms" in RFC4253.

string "ssh-rsa" mpint e mpint n

The format of this "string" or "mpint" is described in "5. Data Type Representations Used in the SSH Protocols" of RFC4251. Simply put, it's like a 4-byte big endian length + bytes.

It seems that this should be encoded in Base64. In the case of OpenSSH format, Base64 seems to have to output one line without wrapping the line.

$ e $ and $ n $ correspond to $ E $ and $ N $ described in the "RSA Key Pair Overview" above.

By the way, the values of $ e $ and $ n $ can be obtained with the getPublicExponent () and getModulus () methods, respectively, for Java RSAPublicKey instances.

(There may be something like an email address at the end of the public key, but this should be a comment)

    public String encodeRsaPublicKey(final RSAPublicKey publicKey) {

        final String sig = "ssh-rsa";
        final byte[] sigBytes = sig.getBytes(StandardCharsets.US_ASCII);
        final byte[] eBytes = publicKey.getPublicExponent().toByteArray();
        final byte[] nBytes = publicKey.getModulus().toByteArray();

        final int size = 4 + sigBytes.length
                + 4 + eBytes.length
                + 4 + nBytes.length;

        final byte[] publicKeyBytes = ByteBuffer.allocate(size)
                .order(ByteOrder.BIG_ENDIAN)
                .putInt(sigBytes.length).put(sigBytes)
                .putInt(eBytes.length).put(eBytes)
                .putInt(nBytes.length).put(nBytes)
                .array();

        final String publicKeyBase64 = Base64.getEncoder()
                .encodeToString(publicKeyBytes);

        final String publicKeyEncoded = sig + " " + publicKeyBase64;
        return publicKeyEncoded;
    }

OpenSSH DSA public key file format (bonus)

DSA is described in RFC4253 as well as RSA.

string "ssh-dss" mpint p mpint q mpint g mpint y

    public String encodeDsaPublicKey(final DSAPublicKey publicKey) {

        final String sig = "ssh-dss";
        final byte[] sigBytes = sig.getBytes(StandardCharsets.US_ASCII);
        final DSAParams params = publicKey.getParams();
        final byte[] pBytes = params.getP().toByteArray();
        final byte[] qBytes = params.getQ().toByteArray();
        final byte[] gBytes = params.getG().toByteArray();
        final byte[] yBytes = publicKey.getY().toByteArray();

        final int size = 4 + sigBytes.length
                + 4 + pBytes.length
                + 4 + qBytes.length
                + 4 + gBytes.length
                + 4 + yBytes.length;

        final byte[] publicKeyBytes = ByteBuffer.allocate(size)
                .order(ByteOrder.BIG_ENDIAN)
                .putInt(sigBytes.length).put(sigBytes)
                .putInt(pBytes.length).put(pBytes)
                .putInt(qBytes.length).put(qBytes)
                .putInt(gBytes.length).put(gBytes)
                .putInt(yBytes.length).put(yBytes)
                .array();

        final String publicKeyBase64 = Base64.getEncoder()
                .encodeToString(publicKeyBytes);

        final String publicKeyEncoded = sig + " " + publicKeyBase64;
        return publicKeyEncoded;
    }

OpenSSH ECDSA (Elliptic Curve Cryptography) Public Key File Format (Bonus)

The format of the public key of elliptic curve cryptography seems to be defined in RFC5656.

string   "ecdsa-sha2-[identifier]"
byte[n]  ecc_key_blob

ʻEcc_key_blob` is inside

string   [identifier]
string   Q

ʻIdentifier seems to be derived from the type of elliptic curve. The NIST part of the table below is ʻidentifier.

NIST SEC OID
nistp256 secp256r1 1.2.840.10045.3.1.7
nistp384 secp384r1 1.3.132.0.34
nistp521 secp521r1 1.3.132.0.35

By the way, these three types seem to be [required] curves (an elliptic curve that must be implemented when ECDSA is supported by the SSH server / client).

There seems to be another [Recommended] elliptic curve, but I will omit it because there are many. By the way, the recommended curve ʻidentifier uses the OID column instead of the NIST column (eg ʻecdsa-sha2-1.3.132.0.38 should be a signature). See RFC for details.

Also, Q is said to be" an encoded point on an elliptic curve into an octet string ", but the details of the specification are http://www.secg.org/download/aid-780/sec1-v2 It is mentioned in .pdf. When I actually searched for it, it seems that it has moved to https://www.secg.org/sec1-v2.pdf. More information can be found in 2.3.3 of this document. It seems that the format differs depending on whether you use the function called "point compression" or not, but if you do not use it, it seems to be a relatively simple structure.

For the time being, if ʻidentifier` is decided, it looks like the following.

    public String encodeEcPublicKey(
            final String identifier,
            final ECPublicKey publicKey) {

        final String sig = "ecdsa-sha2-" + identifier;
        final BigInteger x = publicKey.getW().getAffineX();
        final BigInteger y = publicKey.getW().getAffineY();

        final byte[] sigBytes = sig.getBytes(StandardCharsets.US_ASCII);
        final byte[] identifierBytes = identifier.getBytes(StandardCharsets.US_ASCII);
        final byte[] xBytes = x.toByteArray();
        final byte[] yBytes = y.toByteArray();

        final int size = 4 + sigBytes.length
                + 4 + identifierBytes.length
                + 4 + 1 + xBytes.length + yBytes.length;

        final byte[] publicKeyBytes = ByteBuffer.allocate(size)
                .order(ByteOrder.BIG_ENDIAN)
                .putInt(sigBytes.length).put(sigBytes)
                .putInt(identifierBytes.length).put(identifierBytes)
                .putInt(1 + xBytes.length + yBytes.length)
                .put((byte) 0x04)
                .put(xBytes)
                .put(yBytes)
                .array();

        final String publicKeyBase64 = Base64.getEncoder()
                .encodeToString(publicKeyBytes);

        final String publicKeyEncoded = sig + " " + publicKeyBase64;
        return publicKeyEncoded;

    }

And ʻidentifier indicates the type of elliptic curve, and it seems that the contents of ʻECParameterSpec that can be obtained from the private key or public key can be used for judgment.

For specific parameters, it seems necessary to see the specifications of the elliptic curve.

Details of the specifications can be found at http://www.secg.org/download/aid-386/sec2_final.pdf. When I actually searched for it, it was also moved https://www.secg.org/SEC2-Ver-1.0.pdf It was in.

For the time being, to determine the required curve ʻidentifier based on ʻECParameterSpec, it looks like the following.

    private Optional<String> decideEcIdentifier(final ECParameterSpec params) {

        if (!(params.getCurve().getField() instanceof ECFieldFp)) {
            return Optional.empty();
        }
        final ECFieldFp field = (ECFieldFp) params.getCurve().getField();
        final BigInteger p = field.getP();
        final BigInteger a = params.getCurve().getA();
        final BigInteger b = params.getCurve().getB();
        final BigInteger g1 = params.getGenerator().getAffineX();
        final BigInteger g2 = params.getGenerator().getAffineY();
        final BigInteger n = params.getOrder();
        final int h = params.getCofactor();

        final List<Number> tuple = Arrays.asList(p, a, b, g1, g2, n, h);
        final String identifier = identifierMap.get(tuple);
        return Optional.ofNullable(identifier);
    }

    private static final Map<List<Number>, String> identifierMap;
    static {
        final Map<List<Number>, String> map = new LinkedHashMap<>();

        map.put(Arrays.asList(
                new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951"),
                new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
                new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"),
                new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
                new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"),
                new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
                1), "nistp256");
        map.put(Arrays.asList(
                new BigInteger(
                        "39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319"),
                new BigInteger(
                        "39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112316"),
                new BigInteger(
                        "27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575"),
                new BigInteger(
                        "26247035095799689268623156744566981891852923491109213387815615900925518854738050089022388053975719786650872476732087"),
                new BigInteger(
                        "8325710961489029985546751289520108179287853048861315594709205902480503199884419224438643760392947333078086511627871"),
                new BigInteger(
                        "39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643"),
                1), "nistp384");
        map.put(Arrays.asList(
                new BigInteger(
                        "6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151"),
                new BigInteger(
                        "6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057148"),
                new BigInteger(
                        "1093849038073734274511112390766805569936207598951683748994586394495953116150735016013708737573759623248592132296706313309438452531591012912142327488478985984"),
                new BigInteger(
                        "2661740802050217063228768716723360960729859168756973147706671368418802944996427808491545080627771902352094241225065558662157113545570916814161637315895999846"),
                new BigInteger(
                        "3757180025770020463545507224491183603594455134769762486694567779615544477440556316691234405012945539562144444537289428522585666729196580810124344277578376784"),
                new BigInteger(
                        "6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449"),
                1), "nistp521");
        identifierMap = Collections.unmodifiableMap(map);
    }

OpenSSH public key file format

This is an example of encoding the above three formats.

    public Optional<String> encodePublicKey(final PublicKey publicKey) {

        if (publicKey instanceof RSAPublicKey) {
            final RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
            return Optional.of(encodeRsaPublicKey(rsaPublicKey));

        } else if (publicKey instanceof DSAPublicKey) {
            final DSAPublicKey dsaPublicKey = (DSAPublicKey) publicKey;
            return Optional.of(encodeDsaPublicKey(dsaPublicKey));

        } else if (publicKey instanceof ECPublicKey) {
            final ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
            return decideEcIdentifier(ecPublicKey.getParams())
                    .map(identifier -> encodeEcPublicKey(identifier, ecPublicKey));
        }
        return Optional.empty();
    }

As a bonus, generate a public key from the RSA private key

If you use ssh-keygen, it looks like the following

ssh-keygen -yf id_rsa > id_rsa.pub

If you do it in Java, it looks like this.

    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(4096);
    KeyPair keyPair = generator.genKeyPair();
    RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyPair.getPrivate();
    BigInteger publicExponent = privateKey.getPublicExponent();
    BigInteger modulus = privateKey.getModulus();
    KeyFactory factory = KeyFactory.getInstance("RSA");
    RSAPublicKeySpec spec = new RSAPublicKeySpec(
            modulus,
            publicExponent);
    RSAPublicKey publicKey = (RSAPublicKey) factory.generatePublic(spec);

The point is that the normally generated RSA private key also implements RSAPrivateCrtKey.

Further bonus

Is ED25519 an RFC not yet published?

Internet Draft [https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-ed448-08](https://tools.ietf.org/html/draft-ietf-curdle-ssh -ed25519-ed448-08) looks like the following.

string "ssh-ed25519" string key

Similarly, ED448 looks like the following.

string "ssh-ed448" string key

Recommended Posts

[memo] Generate RSA key pair for SSH in Java
Key points for introducing gRPC in Java
RSA key pair creation / encryption / decryption sample (JAVA)
(Memo) Java for statement
How to generate / verify ID token in Java Memo
Rock-paper-scissors game for beginners in Java
[For beginners] Run Selenium in Java
Encrypt using RSA cryptography in Java
Settings for SSL debugging in Java
How to encrypt and decrypt with RSA public key in Java
Duplicate Map sorted by key in Java
First steps for deep learning in Java
[Java] for Each and sorted in Lambda
Organized memo in the head (Java --Array)
Memo for migration from java to kotlin
Display message dialog in java (personal memo)
Java memo
ChatWork4j for using the ChatWork API in Java
Technology for reading Java source code in Eclipse
Solution for NetBeans 8.2 not working in Java 9 environment
Organized memo in the head (Java --instance edition)
SourceMapPathOverrides setting memo in VSCode Debugger for Chrome
Organized memo in the head (Java --Data type)
Set pop-up display for Java language in vim.
Generate Java client code for Salesforce SOAP API
Generate OffsetDateTime from Clock and LocalDateTime in Java
Compare PDF output in Java for snapshot testing
Enable / disable SNI in Java for each communication
Things to watch out for in Java equals