1

I need to generate an openssh format key pair in java.

I generate a public private key pair using the KeyPairGenerator in java and save it to a file:

    gen.initialize(2048);
    KeyPair pair = gen.generateKeyPair();
    Base64.Encoder encoder = Base64.getEncoder();
    
    PrivateKey privateKey = pair.getPrivate();
    PublicKey publicKey = pair.getPublic();
    System.out.println(encoder.encodeToString(publicKey.getEncoded());
    System.out.println(encoder.encodeToString(privateKey.getEncoded()); 

However, if I try to use this with ssh-keygen I get a "Failed to load key private.key: invalid format"

How can I either convert the PKCS#8 private key to openssh format or generate a key pair in the openssh format?

EDIT: To clarify, I am looking for something like this:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAo+2pHTLFxKmzyMZC4VVnrFTTHONdoKRYPeT4+ohK/g2X7U8aBj1V
FyQntFUMIRHgQcZcGQX0tLpnv2J5Fyr1YfxCakLB6W5XIAHIwuSRO4H6YpX4iMW7C7RWsq
Q2JUW5Vab89WduAotltIjLOyRUO2E8LWtOcCnnk1j+tVaB6q+EvK0MNxU5JOtybRkQWJTa
ONkBuZMeJ4e2Et/WVNceY6ZmBlMEVVW9uQ4j0MQk3UoWkY4JdPinIGi1Do2xUfuTwbmE0w
y4yb5yMw8fZvVDxFqNJSNLHFUihzsRMgbDRQ+P30e9uFKBBJ0pe2ZArYWLA40ojPzzi2ob
oB+NwSVHDQAAA7iTErFrkxKxawAAAAdzc2gtcnNhAAABAQCj7akdMsXEqbPIxkLhVWesVN
Mc412gpFg95Pj6iEr+DZftTxoGPVUXJCe0VQwhEeBBxlwZBfS0ume/YnkXKvVh/EJqQsHp
blcgAcjC5JE7gfpilfiIxbsLtFaypDYlRblVpvz1Z24Ci2W0iMs7JFQ7YTwta05wKeeTWP
61VoHqr4S8rQw3FTkk63JtGRBYlNo42QG5kx4nh7YS39ZU1x5jpmYGUwRVVb25DiPQxCTd
ShaRjgl0+KcgaLUOjbFR+5PBuYTTDLjJvnIzDx9m9UPEWo0lI0scVSKHOxEyBsNFD4/fR7
24UoEEnSl7ZkCthYsDjSiM/POLahugH43BJUcNAAAAAwEAAQAAAQEAn+Qoxn0GX4sy+8s9
4rG93F4kSJIQeaazFzPmEd+sXd5+aI52EM3z2A2A2Kj3mq3n8d/7ZsDjbQBAP3FaMNnK3B
cD5MdWgkwImQSEgGwWqFdgFJa5AxbyGTl+MuJuma5HVp75LpgCumKjAhNHP1lw+zYdTyPS
Lx8AbD0qu080iuWtMwWV5Hap9ZHjYEVIrgDArxbzTT0wvpqT1cFCon9vJVJpPOWsRbpEgt
d+M+eOpBL7mDKJhub9soguhhRxqgYvgigJueeqMnYpxPye7oewratTMAWskk0n0zXKRON0
q9zVVfdc9Md0cqkkbNGyQ/VdbrM+zZt+46v0zgyk6zdrdQAAAIAcnNjD6Pm8YFXa2mkI47
m+2SRcKJ4jmvGJTN8iRan7nmBMNDfC4EbGW8eQbcl4PsUevqd0O2PUpDNL5m9stAF8vBna
QRlzlWNncdJTBS4Rlzf+LisCrcecdl+Vg1tSLpur/+prk9VFZDnj5uUPSHb3zdLfYt7onV
VA0FSm0PeFdwAAAIEA/YQw567hHMp2mqaxW3ogNsqSv+nRiZEPqEHvCW3znVMJZPsbubpg
MiJaQIfi1fNkgPu3pKdCu1M1+tf4hKK5Tjh8jPh9KSi/T6VZ4lQbFZaMFWXeY0FKKszkGR
b+8BHHdH9a8XnAJvwWOGN5evJl69j7VylMB0ZIIZHiEt/ZDIsAAACBAKWIyVHk+RLz/maM
11lajRrnM46YXRJR4pm0AWQLcso74Q5DMhi0I6joUyrTPliUKcWM38lCN3uKV5qBioCzJv
PfWgDquQCj7kMCC98CA3YSbffeLtMigon+kT3JZb9tYKbTyRx35yw+6ghbi7ccWZgLoz6w
g6ulyyuhmYLHM3XHAAAAAAEC
-----END OPENSSH PRIVATE KEY-----

versus this:

-----BEGIN RSA PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCj7akdMsXEqbPI
xkLhVWesVNMc412gpFg95Pj6iEr+DZftTxoGPVUXJCe0VQwhEeBBxlwZBfS0ume/
YnkXKvVh/EJqQsHpblcgAcjC5JE7gfpilfiIxbsLtFaypDYlRblVpvz1Z24Ci2W0
iMs7JFQ7YTwta05wKeeTWP61VoHqr4S8rQw3FTkk63JtGRBYlNo42QG5kx4nh7YS
39ZU1x5jpmYGUwRVVb25DiPQxCTdShaRjgl0+KcgaLUOjbFR+5PBuYTTDLjJvnIz
Dx9m9UPEWo0lI0scVSKHOxEyBsNFD4/fR724UoEEnSl7ZkCthYsDjSiM/POLahug
H43BJUcNAgMBAAECggEBAJ/kKMZ9Bl+LMvvLPeKxvdxeJEiSEHmmsxcz5hHfrF3e
fmiOdhDN89gNgNio95qt5/Hf+2bA420AQD9xWjDZytwXA+THVoJMCJkEhIBsFqhX
YBSWuQMW8hk5fjLibpmuR1ae+S6YArpiowITRz9ZcPs2HU8j0i8fAGw9KrtPNIrl
rTMFleR2qfWR42BFSK4AwK8W8009ML6ak9XBQqJ/byVSaTzlrEW6RILXfjPnjqQS
+5gyiYbm/bKILoYUcaoGL4IoCbnnqjJ2KcT8nu6HsK2rUzAFrJJNJ9M1ykTjdKvc
1VX3XPTHdHKpJGzRskP1XW6zPs2bfuOr9M4MpOs3a3UCgYEA/YQw567hHMp2mqax
W3ogNsqSv+nRiZEPqEHvCW3znVMJZPsbubpgMiJaQIfi1fNkgPu3pKdCu1M1+tf4
hKK5Tjh8jPh9KSi/T6VZ4lQbFZaMFWXeY0FKKszkGRb+8BHHdH9a8XnAJvwWOGN5
evJl69j7VylMB0ZIIZHiEt/ZDIsCgYEApYjJUeT5EvP+ZozXWVqNGuczjphdElHi
mbQBZAtyyjvhDkMyGLQjqOhTKtM+WJQpxYzfyUI3e4pXmoGKgLMm899aAOq5AKPu
QwIL3wIDdhJt994u0yKCif6RPcllv21gptPJHHfnLD7qCFuLtxxZmAujPrCDq6XL
K6GZgsczdccCgYEAu1VvfgRwPIIv2l/LNzmrjFFs13vEZs9WrpLqPCGPn3W4v3H/
LuNWKjXQU1cWe9r7LYTUU0t1uE7o2I+3COvl2rNK9jC47C08EXKyVNipmu5AXZ+F
Efsw/yegdhnUETpSFPf3D/FT2Hr3QHvhTMTKI9mXAPV3RPjeXFAcqq3XCbkCgYBI
2scgUBJ/kPuqztoI7Z2k9ZTvcYelBH0jAOKL0a4X6/rFeDWYQdBgCsBv3MFX4v3v
gG0N+yLIML8VtWXr9u1x8B+Av83kxkGbJE9tO0miscHMkfEx48JoUa5C71zkv5MG
Wbft26fXBWmjfAcl9EhKbvTOJooNBc9ByMHzmRDBCQKBgByc2MPo+bxgVdraaQjj
ub7ZJFwoniOa8YlM3yJFqfueYEw0N8LgRsZbx5BtyXg+xR6+p3Q7Y9SkM0vmb2y0
AXy8GdpBGXOVY2dx0lMFLhGXN/4uKwKtx5x2X5WDW1Ium6v/6muT1UVkOePm5Q9I
dvfN0t9i3uidVUDQVKbQ94V3
-----END RSA PRIVATE KEY----- 
John
  • 53
  • 1
  • 6
  • But why `Base64.Encoder`? – Martin Zeitler Mar 16 '22 at 14:42
  • To get the key in string format, and not just the modulus and exponent. – John Mar 16 '22 at 15:13
  • Either you provide the DER encoded key or you provide a PEM encoded key. But in the last case the base 64 should be surrounded by the header & footer lines indicating "PRIVATE KEY" (**not** RSA PRIVATE KEY). Best to use a PEM writer such as the one found in e.g. Bouncy Castle. – Maarten Bodewes Mar 16 '22 at 15:41
  • Is there a way to do it without using BouncyCastle? – John Mar 16 '22 at 16:18
  • @Topaco: while ssh-keygen only writes OpenSSL 'traditional' formats and OpenSSH's 'new' format (since 6.5 in 2014), it (and ssh and sshd) has always been able to _read_ PKCS8 -- but only as proper PEM (with header and footer as stated by Maarten AND line breaks as stated in rfc1421 or rfc7468). – dave_thompson_085 Mar 16 '22 at 16:38

1 Answers1

2

For the private key, PKCS8 PEM is compatible with OpenSSH. (Contrary to Maarten you can't use DER, but contrary to Topaco you aren't required to convert to PKCS1 'traditional' format.) You do need to create correct PEM, which requires header and footer lines AND linebreaks in the body (not a single humongous string); see wikipedia or rfc7468. Java 8+ Base64.getMimeEncoder() does the linebreaks.

PrivateKey pkey = pair.getPrivate();
System.out.println ("-----BEGIN PRIVATE KEY-----");
System.out.println (Base64.getMimeEncoder().encodeToString(pkey.getEncoded()));
// to be strict use getMimeEncoder(64, value_of_line_separator)
// for PEM, but in practice MIME default 76 actually works
System.out.println ("-----END PRIVATE KEY-----");

Note this format is unencrypted, thus anyone who gets/sees your file (or a copy or backup of it) gets your private key. Whether, when and how this is a problem depends on many factors and belongs on security.SX not SO. OpenSSH supports and generally encourages encrypted privatekey files, but (also) supports unencrypted ones.

Public key is harder. OpenSSH's public key format does not follow the standards supported by Java (without adding BouncyCastle). However, you can construct this by hand, and this has been answered several times; see my list at ssh-keygen and openssl gives two different public keys and example in How to Calculate Fingerprint From SSH RSA Public Key in Java? (which references the same). Note the X.509 SubjectPublicKeyInfo format preferred by OpenSSL is the same format used by Java's PublicKey.getEncoded() (which Java's .getFormat() ambiguously calls "X.509"), except that Java uses binary/DER only while OpenSSL uses both binary/DER and PEM.

Update: your complaint was 'fails to load in ssh-keygen' so I thought you could use any format accepted by OpenSSH, which the PKCS8 format above is. If you insist on the OpenSSH-defined 'new' format, even though it isn't necessary for OpenSSH including ssh-keygen, that's much harder in plain Java without BouncyCastle. So far I've got the case you now show (RSA unencrypted) only:

        RSAPrivateCrtKey pkey = // generated or read (I used test data I already have)
        
        byte[] alg = "ssh-rsa".getBytes(), none = "none".getBytes();
        byte[] nbyt = pkey.getModulus().toByteArray(), ebyt = pkey.getPublicExponent().toByteArray();
        int rand = new Random().nextInt();
        
        ByteBuffer pub = ByteBuffer.allocate(nbyt.length+50); // always enough, but not too much over
        for( byte[] x : new byte[][]{alg,ebyt,nbyt} )
        { pub.putInt(x.length); pub.put(x); }
        
        ByteBuffer prv = ByteBuffer.allocate(nbyt.length*4+50); // ditto
        prv.putInt(rand); prv.putInt(rand);
        for( byte[] x : new byte[][]{alg,nbyt,ebyt,pkey.getPrivateExponent().toByteArray(),
            pkey.getCrtCoefficient().toByteArray(),pkey.getPrimeP().toByteArray(),pkey.getPrimeQ().toByteArray()} )
        { prv.putInt(x.length); prv.put(x); }
        prv.putInt(0); // no comment
        for( int i = 0; prv.position()%8 != 0; ) prv.put((byte)++i); // 8 apparently default? IDK
        
        ByteBuffer all = ByteBuffer.allocate(100+pub.position()+prv.position()); // ditto
        all.put("openssh-key-v1".getBytes()); all.put((byte)0); 
        all.putInt(none.length); all.put(none); // cipher
        all.putInt(none.length); all.put(none); // pbkdf
        all.putInt(0); all.putInt(1); // parms, count
        all.putInt(pub.position()); all.put(pub.array(),0,pub.position());
        all.putInt(prv.position()); all.put(prv.array(),0,prv.position());
        byte[] result = Arrays.copyOf(all.array(),  all.position());
        
        System.out.print ("-----BEGIN OPENSSH PRIVATE KEY-----\n"
                + Base64.getMimeEncoder(68,"\n".getBytes()).encodeToString(result)
                + "\n-----END OPENSSH PRIVATE KEY-----\n");
        // MimeEncoder only does multiple of 4 char, consistent with the 3-to-4 logic
        // OpenSSH writes 70 but accepts 68; it requires no-CR (contrary to MIME) 
dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
  • So this does not give me what I need. The only thing that the MimeEncoder does is break it up into 64 characters per line. While this is a requirement for openssh format, it is not in the right format. Example. The key still starts with a "MIIEvQIBADANB...." vs. starting with "b3BlbnNzaC1r...." – John Mar 18 '22 at 18:37