I'm pretty much novice in cryptography and has an issue with loading a ed25519
public key string into java.security.PublicKey
. It works when I try a key using openssl
, but not when I load a key which generated using ssh-keygen
. To give more context, we have a apache-mina
SFTP Server where the clients can log in using public keys.
The code is below (written in Scala), I maybe doing a fundamental mistake, I just don't see it. Any help is appreciated.
def main(args: Array[String]): Unit = {
// ssh key generated from openssl:
// openssl genpkey -algorithm ed25519 -out private_key.pem
// openssl pkey -in private_key.pem -pubout -out public_key.pem
val sshKey = "MCowBQYDK2VwAyEA4LSmWoy4ZBYJuwRttwzSLu0KQAYBKGRMHqPNBAun0gA="
println(stringToKey(sshKey))
// ssh key generated using ssh-keygen (ssh-keygen -t ed25519 -C "martin")
val sshKeyGen = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpjh408HMAa1uE60DFrUs0GcgRflP1Hc3iLJlesVfCb martin"
val Array(_, keyData, _) = sshKeyGen.split(" ", 3) // Split the key
println(stringToKey(keyData)) // Not working
}
def stringToKey(sshKey: String): PublicKey = {
Security.addProvider(new BouncyCastleProvider())
val serializedKey: Array[Byte] = Base64.getDecoder.decode(sshKey)
val kf: KeyFactory = KeyFactory.getInstance("Ed25519", "BC")
val keySpec: X509EncodedKeySpec = new X509EncodedKeySpec(serializedKey)
val key: PublicKey = kf.generatePublic(keySpec)
key
}
Output
**OpenSSL Key**
Ed25519 Public Key [70:e6:65:14:c0:99:2d:13:58:87:e1:cc:95:9b:2c:93:39:03:39:9f]
public data: e0b4a65a8cb8641609bb046db70cd22eed0a40060128644c1ea3cd040ba7d200
**SSH Keygen Key**
Exception in thread "main" java.security.spec.InvalidKeySpecException: encoded key spec not recognized: failed to construct sequence from byte[]: unexpected end-of-contents marker
at org.bouncycastle.jcajce.provider.asymmetric.util.BaseKeyFactorySpi.engineGeneratePublic(Unknown Source)
at org.bouncycastle.jcajce.provider.asymmetric.edec.KeyFactorySpi.engineGeneratePublic(KeyFactorySpi.java:224)
at java.base/java.security.KeyFactory.generatePublic(KeyFactory.java:345)
The OpenSSH format for publickeys is nonstandard.
For publickeys for all algorithms, OpenSSL uses the ASN.1 structure SubjectPublicKeyInfo
defined by X.509/PKIX in RFC5280 which is also what Java crypto natively uses. Although 'SPKI' in general can be complex, the Ed25519 case is pretty simple. OpenSSH uses its own format, based on the SSH protocol wire formats, and for it also Ed25519 is simple.
You can thus convert an OpenSSH-format Ed25519 key to the form Java supports as follows:
// in Java because I don't scale, but you should be able to convert
String osshpub = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpjh408HMAa1uE60DFrUs0GcgRflP1Hc3iLJlesVfCb martin";
byte[] rawpub = Base64.getDecoder().decode( osshpub.split(" ")[1] );
byte[] prefix = { 0x30,0x2a,0x30,0x05,0x06,0x03,0x2b,0x65,0x70,0x03,0x21,0x00 };
// or equivalent and perhaps easier
//byte[] prefix = Base64.getDecoder().decode("MCowBQYDK2VwAyEA");
byte[] spki = Arrays.copyOf(prefix, prefix.length+32);
System.arraycopy(rawpub,19, spki,prefix.length, 32);
PublicKey javapub = KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(spki));
// in Java 15 up the standard provider(s) support Ed25519 and you don't need BouncyCastle
// below that or to force Bouncy, add ,"BC" in getInstance
But if using Apache mina you don't need to; it supports the OpenSSH 'authorized_keys' format, which this is, by itself; see e.g. the overloads of readAuthorizedKeys
in https://github.com/apache/mina-sshd/blob/master/sshd-common/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java . The 'known_hosts' format used by clients has a field stuck in front (for the hostname(s)/address(es)) but is otherwise the same, and is also supported.