mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-07 15:50:57 +03:00
Added support for ed25519 keys (Fixes #220)
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
package com.hierynomus.sshj.signature;
|
||||||
|
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||||
|
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our own extension of the EdDSAPublicKey that comes from ECC-25519, as that class does not implement equality.
|
||||||
|
* The code uses the equality of the keys as an indicator whether they're the same during host key verification.
|
||||||
|
*/
|
||||||
|
public class Ed25519PublicKey extends EdDSAPublicKey {
|
||||||
|
|
||||||
|
public Ed25519PublicKey(EdDSAPublicKeySpec spec) {
|
||||||
|
super(spec);
|
||||||
|
|
||||||
|
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("ed25519-sha-512");
|
||||||
|
if (!spec.getParams().getCurve().equals(ed25519.getCurve())) {
|
||||||
|
throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof Ed25519PublicKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ed25519PublicKey otherKey = (Ed25519PublicKey) other;
|
||||||
|
return Arrays.equals(getAbyte(), otherKey.getAbyte());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getA().hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.hierynomus.sshj.signature;
|
||||||
|
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||||
|
import net.schmizz.sshj.common.Buffer;
|
||||||
|
import net.schmizz.sshj.common.KeyType;
|
||||||
|
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||||
|
import net.schmizz.sshj.signature.Signature;
|
||||||
|
|
||||||
|
import java.security.*;
|
||||||
|
|
||||||
|
public class SignatureEdDSA implements Signature {
|
||||||
|
public static class Factory implements net.schmizz.sshj.common.Factory.Named<Signature> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return KeyType.ED25519.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Signature create() {
|
||||||
|
return new SignatureEdDSA();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final EdDSAEngine engine;
|
||||||
|
|
||||||
|
protected SignatureEdDSA() {
|
||||||
|
try {
|
||||||
|
engine = new EdDSAEngine(MessageDigest.getInstance("SHA-512"));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new SSHRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(PublicKey pubkey, PrivateKey prvkey) {
|
||||||
|
try {
|
||||||
|
if (pubkey != null) {
|
||||||
|
engine.initVerify(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prvkey != null) {
|
||||||
|
engine.initSign(prvkey);
|
||||||
|
}
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new SSHRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(byte[] H) {
|
||||||
|
update(H, 0, H.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(byte[] H, int off, int len) {
|
||||||
|
try {
|
||||||
|
engine.update(H, off, len);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SSHRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] sign() {
|
||||||
|
try {
|
||||||
|
return engine.sign();
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SSHRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encode(byte[] signature) {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verify(byte[] sig) {
|
||||||
|
try {
|
||||||
|
Buffer.PlainBuffer plainBuffer = new Buffer.PlainBuffer(sig);
|
||||||
|
String algo = plainBuffer.readString();
|
||||||
|
if (!"ssh-ed25519".equals(algo)) {
|
||||||
|
throw new SSHRuntimeException("Expected 'ssh-ed25519' key algorithm, but was: " + algo);
|
||||||
|
}
|
||||||
|
byte[] bytes = plainBuffer.readBytes();
|
||||||
|
return engine.verify(bytes);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SSHRuntimeException(e);
|
||||||
|
} catch (Buffer.BufferException e) {
|
||||||
|
throw new SSHRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj;
|
package net.schmizz.sshj;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.signature.SignatureEdDSA;
|
||||||
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
||||||
import com.hierynomus.sshj.transport.cipher.StreamCiphers;
|
import com.hierynomus.sshj.transport.cipher.StreamCiphers;
|
||||||
import net.schmizz.keepalive.KeepAliveProvider;
|
import net.schmizz.keepalive.KeepAliveProvider;
|
||||||
@@ -181,7 +182,7 @@ public class DefaultConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void initSignatureFactories() {
|
protected void initSignatureFactories() {
|
||||||
setSignatureFactories(new SignatureECDSA.Factory(), new SignatureRSA.Factory(), new SignatureDSA.Factory());
|
setSignatureFactories(new SignatureECDSA.Factory(), new SignatureRSA.Factory(), new SignatureDSA.Factory(), new SignatureEdDSA.Factory());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void initMACFactories() {
|
protected void initMACFactories() {
|
||||||
|
|||||||
@@ -16,6 +16,13 @@
|
|||||||
package net.schmizz.sshj.common;
|
package net.schmizz.sshj.common;
|
||||||
|
|
||||||
import com.hierynomus.sshj.secg.SecgUtils;
|
import com.hierynomus.sshj.secg.SecgUtils;
|
||||||
|
import com.hierynomus.sshj.signature.Ed25519PublicKey;
|
||||||
|
import net.i2p.crypto.eddsa.*;
|
||||||
|
import net.i2p.crypto.eddsa.math.GroupElement;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||||
import org.bouncycastle.asn1.nist.NISTNamedCurves;
|
import org.bouncycastle.asn1.nist.NISTNamedCurves;
|
||||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||||
@@ -26,6 +33,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
|
import java.security.KeyFactory;
|
||||||
import java.security.interfaces.*;
|
import java.security.interfaces.*;
|
||||||
import java.security.spec.DSAPublicKeySpec;
|
import java.security.spec.DSAPublicKeySpec;
|
||||||
import java.security.spec.RSAPublicKeySpec;
|
import java.security.spec.RSAPublicKeySpec;
|
||||||
@@ -163,18 +171,43 @@ public enum KeyType {
|
|||||||
protected boolean isMyType(Key key) {
|
protected boolean isMyType(Key key) {
|
||||||
return ("ECDSA".equals(key.getAlgorithm()));
|
return ("ECDSA".equals(key.getAlgorithm()));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
private byte[] trimStartingZeros(byte[] in) {
|
ED25519("ssh-ed25519") {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(KeyType.class);
|
||||||
|
@Override
|
||||||
|
public PublicKey readPubKeyFromBuffer(String type, Buffer<?> buf) throws GeneralSecurityException {
|
||||||
|
try {
|
||||||
|
final int keyLen = buf.readUInt32AsInt();
|
||||||
|
final byte[] p = new byte[keyLen];
|
||||||
|
buf.readRawBytes(p);
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug(String.format("Key algo: %s, Key curve: 25519, Key Len: %s\np: %s",
|
||||||
|
type,
|
||||||
|
keyLen,
|
||||||
|
Arrays.toString(p))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
int i = 0;
|
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("ed25519-sha-512");
|
||||||
for (; i < in.length; i++) {
|
GroupElement point = ed25519.getCurve().createPoint(p, true);
|
||||||
if (in[i] != 0) {
|
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(point, ed25519);
|
||||||
break;
|
return new Ed25519PublicKey(publicSpec);
|
||||||
|
|
||||||
|
} catch (Buffer.BufferException be) {
|
||||||
|
throw new SSHRuntimeException(be);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final byte[] out = new byte[in.length - i];
|
|
||||||
System.arraycopy(in, i, out, 0, out.length);
|
@Override
|
||||||
return out;
|
public void putPubKeyIntoBuffer(PublicKey pk, Buffer<?> buf) {
|
||||||
|
EdDSAPublicKey key = (EdDSAPublicKey) pk;
|
||||||
|
buf.putString(sType).putBytes(key.getAbyte());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isMyType(Key key) {
|
||||||
|
return "EdDSA".equals(key.getAlgorithm());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -133,13 +133,23 @@ public class OpenSSHKeyFileTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldHaveCorrectFingerprintForECDSA() throws IOException, GeneralSecurityException {
|
public void shouldHaveCorrectFingerprintForECDSA() throws IOException, GeneralSecurityException {
|
||||||
OpenSSHKeyFile keyFile = new OpenSSHKeyFile();
|
OpenSSHKeyFile keyFile = new OpenSSHKeyFile();
|
||||||
keyFile.init(new File("src/test/resources/test_ecdsa_nistp256"));
|
keyFile.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256"));
|
||||||
String expected = "256 MD5:53:ae:db:ed:8f:2d:02:d4:d5:6c:24:bc:a4:66:88:79 root@itgcpkerberosstack-cbgateway-0-20151117031915 (ECDSA)\n";
|
String expected = "256 MD5:53:ae:db:ed:8f:2d:02:d4:d5:6c:24:bc:a4:66:88:79 root@itgcpkerberosstack-cbgateway-0-20151117031915 (ECDSA)\n";
|
||||||
PublicKey aPublic = keyFile.getPublic();
|
PublicKey aPublic = keyFile.getPublic();
|
||||||
String sshjFingerprintSshjKey = net.schmizz.sshj.common.SecurityUtils.getFingerprint(aPublic);
|
String sshjFingerprintSshjKey = net.schmizz.sshj.common.SecurityUtils.getFingerprint(aPublic);
|
||||||
assertThat(expected, containsString(sshjFingerprintSshjKey));
|
assertThat(expected, containsString(sshjFingerprintSshjKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldHaveCorrectFingerprintForED25519() throws IOException {
|
||||||
|
OpenSSHKeyFile keyFile = new OpenSSHKeyFile();
|
||||||
|
keyFile.init(new File("src/test/resources/keytypes/test_ed25519"));
|
||||||
|
String expected = "256 MD5:d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32 root@sshj (ED25519)\n";
|
||||||
|
PublicKey aPublic = keyFile.getPublic();
|
||||||
|
String sshjFingerprintSshjKey = net.schmizz.sshj.common.SecurityUtils.getFingerprint(aPublic);
|
||||||
|
assertThat(expected, containsString(sshjFingerprintSshjKey));
|
||||||
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup()
|
public void setup()
|
||||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
throws UnsupportedEncodingException, GeneralSecurityException {
|
||||||
|
|||||||
7
src/test/resources/keytypes/test_ed25519
Normal file
7
src/test/resources/keytypes/test_ed25519
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAwHSYkZJATPMgvLHkxKAJ9j38Gyyq5HGoWdMcT6FiAiQAAAJDimgR84poE
|
||||||
|
fAAAAAtzc2gtZWQyNTUxOQAAACAwHSYkZJATPMgvLHkxKAJ9j38Gyyq5HGoWdMcT6FiAiQ
|
||||||
|
AAAECmsckQycWnfGQK6XtQpaMGODbAkMQOdJNK6XJSipB7dDAdJiRkkBM8yC8seTEoAn2P
|
||||||
|
fwbLKrkcahZ0xxPoWICJAAAACXJvb3RAc3NoagECAwQ=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
src/test/resources/keytypes/test_ed25519.pub
Normal file
1
src/test/resources/keytypes/test_ed25519.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPoWICJ root@sshj
|
||||||
Reference in New Issue
Block a user