By the way, if you normally use ssh-keygen
, it looks like the following.
ssh-keygen -t rsa -b 4096
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)
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);
}
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)
string" ssh-rsa "
above), and apart from that, before base64. (The part mentioned at the beginning, "There is no delimiter, but the signature" ssh-rsa "is added at the beginning"). I think it's easier to understand if you look at the following sources. 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;
}
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;
}
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);
}
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();
}
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
.
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