working on EdEC (JDK based ed25519)

This commit is contained in:
Jörg Prante 2024-05-26 18:25:20 +02:00
parent 5c18f0704f
commit be26f3e231
7 changed files with 503 additions and 64 deletions

View file

@ -1,4 +1,6 @@
module org.xbib.files.sftp { module org.xbib.files.sftp {
requires org.xbib.net.security;
requires java.logging;
exports org.apache.sshd.client; exports org.apache.sshd.client;
exports org.apache.sshd.client.auth; exports org.apache.sshd.client.auth;
exports org.apache.sshd.client.auth.password; exports org.apache.sshd.client.auth.password;
@ -79,7 +81,5 @@ module org.xbib.files.sftp {
exports org.apache.sshd.common.util.security; exports org.apache.sshd.common.util.security;
exports org.apache.sshd.common.util.security.eddsa; exports org.apache.sshd.common.util.security.eddsa;
exports org.apache.sshd.common.util.threads; exports org.apache.sshd.common.util.threads;
requires org.xbib.net.security;
requires java.logging;
uses org.apache.sshd.common.io.IoServiceFactoryFactory; uses org.apache.sshd.common.io.IoServiceFactoryFactory;
} }

View file

@ -0,0 +1,89 @@
package org.apache.sshd.common.util.security.edec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.EdECPublicKey;
import java.security.spec.EdECPoint;
import java.security.spec.EdECPrivateKeySpec;
import java.security.spec.EdECPublicKeySpec;
import java.security.spec.NamedParameterSpec;
import java.util.Collections;
import java.util.Map;
import org.apache.sshd.common.config.keys.KeyEntryResolver;
import org.apache.sshd.common.config.keys.impl.AbstractPublicKeyEntryDecoder;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.SessionContext;
public final class Ed25519PublicKeyDecoder extends AbstractPublicKeyEntryDecoder<EdECPublicKey, EdECPrivateKey> {
public static final int MAX_ALLOWED_SEED_LEN = 1024;
public static final Ed25519PublicKeyDecoder INSTANCE = new Ed25519PublicKeyDecoder();
private Ed25519PublicKeyDecoder() {
super(EdECPublicKey.class, EdECPrivateKey.class, Collections.singletonList(KeyPairProvider.SSH_ED25519));
}
@Override
public EdECPublicKey clonePublicKey(EdECPublicKey key) throws GeneralSecurityException {
if (key == null) {
return null;
} else {
NamedParameterSpec namedParameterSpec = NamedParameterSpec.ED25519;
EdECPoint edECPoint = key.getPoint();
return generatePublicKey(new EdECPublicKeySpec(namedParameterSpec, edECPoint));
}
}
@Override
public EdECPrivateKey clonePrivateKey(EdECPrivateKey key) throws GeneralSecurityException {
if (key == null) {
return null;
} else {
NamedParameterSpec namedParameterSpec = NamedParameterSpec.ED25519;
return generatePrivateKey(new EdECPrivateKeySpec(namedParameterSpec, key.getEncoded()));
}
}
@Override
public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException {
return KeyPairGenerator.getInstance("Ed25519");
}
@Override
public String encodePublicKey(OutputStream s, EdECPublicKey key) throws IOException {
// TODO
return "ssh-ed25519";
}
@Override
public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException {
return KeyFactory.getInstance("EdDSA");
}
@Override
public EdECPublicKey decodePublicKey(SessionContext session,
String keyType,
InputStream keyData,
Map<String, String> headers)
throws IOException, GeneralSecurityException {
byte[] pk = KeyEntryResolver.readRLEBytes(keyData, MAX_ALLOWED_SEED_LEN);
boolean xisodd = false;
int lastbyteInt = pk[pk.length - 1];
if ((lastbyteInt & 255) >> 7 == 1) {
xisodd = true;
}
pk[pk.length - 1] &= 127;
BigInteger y = new BigInteger(1, pk);
NamedParameterSpec namedParameterSpec = NamedParameterSpec.ED25519;
EdECPoint ep = new EdECPoint(xisodd, y);
EdECPublicKeySpec spec = new EdECPublicKeySpec(namedParameterSpec, ep);
return EdECPublicKey.class.cast(generatePublicKey(spec));
}
}

View file

@ -0,0 +1,166 @@
package org.apache.sshd.common.util.security.edec;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.EdECPublicKey;
import java.security.spec.EdECPoint;
import java.security.spec.EdECPrivateKeySpec;
import java.security.spec.EdECPublicKeySpec;
import java.security.spec.NamedParameterSpec;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.pem.AbstractPEMResourceKeyPairParser;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
import org.apache.sshd.common.util.io.der.ASN1Object;
import org.apache.sshd.common.util.io.der.ASN1Type;
import org.apache.sshd.common.util.io.der.DERParser;
public class EdECPEMResourceKeyParser extends AbstractPEMResourceKeyPairParser {
public static final String BEGIN_MARKER = "BEGIN OPENSSH PRIVATE KEY";
public static final List<String> BEGINNERS = Collections.singletonList(BEGIN_MARKER);
public static final String END_MARKER = "END OPENSSH PRIVATE KEY";
public static final List<String> ENDERS = Collections.singletonList(END_MARKER);
/**
* @see <A HREF="https://tools.ietf.org/html/rfc8410#section-3">RFC8412 section 3</A>
*/
public static final String ED25519_OID = "1.3.101.112";
public static final EdECPEMResourceKeyParser INSTANCE = new EdECPEMResourceKeyParser();
public EdECPEMResourceKeyParser() {
super("Ed25519", ED25519_OID, BEGINNERS, ENDERS);
}
@Override
public Collection<KeyPair> extractKeyPairs(SessionContext session,
NamedResource resourceKey,
String beginMarker, String endMarker,
FilePasswordProvider passwordProvider,
InputStream stream,
Map<String, String> headers) throws IOException, GeneralSecurityException {
KeyPair kp = parseEd25519KeyPair(stream, false);
return Collections.singletonList(kp);
}
private static KeyPair parseEd25519KeyPair(InputStream inputStream,
boolean okToClose) throws IOException, GeneralSecurityException {
try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) {
return parseKeyPair(parser);
}
}
/*
* See https://tools.ietf.org/html/rfc8410#section-7
*
* SEQUENCE {
* INTEGER 0x00 (0 decimal)
* SEQUENCE {
* OBJECTIDENTIFIER 1.3.101.112
* }
* OCTETSTRING keyData
* }
*
* NOTE: there is another variant that also has some extra parameters
* but it has the same "prefix" structure so we don't care
*/
private static KeyPair parseKeyPair(DERParser parser) throws IOException, GeneralSecurityException {
ASN1Object obj = parser.readObject();
if (obj == null) {
throw new StreamCorruptedException("Missing version value");
}
BigInteger version = obj.asInteger();
if (!BigInteger.ZERO.equals(version)) {
throw new StreamCorruptedException("Invalid version: " + version);
}
obj = parser.readObject();
if (obj == null) {
throw new StreamCorruptedException("Missing OID container");
}
ASN1Type objType = obj.getObjType();
if (objType != ASN1Type.SEQUENCE) {
throw new StreamCorruptedException("Unexpected OID object type: " + objType);
}
List<Integer> curveOid;
try (DERParser oidParser = obj.createParser()) {
obj = oidParser.readObject();
if (obj == null) {
throw new StreamCorruptedException("Missing OID value");
}
curveOid = obj.asOID();
}
String oid = GenericUtils.join(curveOid, '.');
// TODO modify if more curves supported
if (!ED25519_OID.equals(oid)) {
throw new StreamCorruptedException("Unsupported curve OID: " + oid);
}
obj = parser.readObject();
if (obj == null) {
throw new StreamCorruptedException("Missing key data");
}
return decodeKeyPair(obj.getValue());
}
private static KeyPair decodeKeyPair(byte[] keyData) throws IOException, GeneralSecurityException {
EdECPrivateKey privateKey = getPrivateKey(keyData);
EdECPublicKey publicKey = getPublicKey(privateKey);
return new KeyPair(publicKey, privateKey);
}
private static EdECPrivateKey getPrivateKey(byte[] keyData) throws IOException, GeneralSecurityException {
try (DERParser parser = new DERParser(keyData)) {
ASN1Object obj = parser.readObject();
if (obj == null) {
throw new StreamCorruptedException("Missing key data container");
}
ASN1Type objType = obj.getObjType();
if (objType != ASN1Type.OCTET_STRING) {
throw new StreamCorruptedException("Mismatched key data container type: " + objType);
}
return generatePrivateKey(obj.getValue());
}
}
private static EdECPrivateKey generatePrivateKey(byte[] seed) throws GeneralSecurityException {
NamedParameterSpec spec = NamedParameterSpec.ED25519;
EdECPrivateKeySpec keySpec = new EdECPrivateKeySpec(spec, seed);
KeyFactory factory = KeyFactory.getInstance("EdDSA");
return EdECPrivateKey.class.cast(factory.generatePrivate(keySpec));
}
private static EdECPublicKey getPublicKey(PrivateKey key) throws GeneralSecurityException {
if (!(key instanceof EdECPrivateKey)) {
throw new InvalidKeyException("Private key is not EdEC private key");
}
byte[] pk = key.getEncoded();
boolean xisodd = false;
int lastbyteInt = pk[pk.length - 1];
if ((lastbyteInt & 255) >> 7 == 1) {
xisodd = true;
}
pk[pk.length - 1] &= 127;
BigInteger y = new BigInteger(1, pk);
NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");
EdECPoint ep = new EdECPoint(xisodd, y);
EdECPublicKeySpec publicKeySpec = new EdECPublicKeySpec(paramSpec, ep);
KeyFactory factory = KeyFactory.getInstance("EdDSA");
return EdECPublicKey.class.cast(factory.generatePublic(publicKeySpec));
}
}

View file

@ -0,0 +1,45 @@
package org.apache.sshd.common.util.security.edec;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import java.util.Objects;
import org.apache.sshd.common.util.security.AbstractSecurityProviderRegistrar;
public class EdECSecurityProviderRegistrar extends AbstractSecurityProviderRegistrar {
public EdECSecurityProviderRegistrar() {
super("Ed25519");
}
@Override
public boolean isEnabled() {
return super.isEnabled();
}
@Override
public Provider getSecurityProvider() {
return Security.getProvider("SunJCE");
}
@Override
public boolean isSecurityEntitySupported(Class<?> entityType, String name) {
if (!isSupported()) {
return false;
}
if (KeyPairGenerator.class.isAssignableFrom(entityType) || KeyFactory.class.isAssignableFrom(entityType)) {
return Objects.compare(name, getName(), String.CASE_INSENSITIVE_ORDER) == 0;
} else if (Signature.class.isAssignableFrom(entityType)) {
return Objects.compare("NONEwithEdDSA", name, String.CASE_INSENSITIVE_ORDER) == 0;
} else {
return false;
}
}
@Override
public boolean isSupported() {
return true;
}
}

View file

@ -0,0 +1,175 @@
package org.apache.sshd.common.util.security.edec;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.EdECPublicKey;
import java.security.spec.EdECPoint;
import java.security.spec.EdECPrivateKeySpec;
import java.security.spec.EdECPublicKeySpec;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.KeyEntryResolver;
import org.apache.sshd.common.config.keys.impl.AbstractPrivateKeyEntryDecoder;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.SecureByteArrayOutputStream;
import org.apache.sshd.common.util.io.der.ASN1Object;
import org.apache.sshd.common.util.io.der.ASN1Type;
import org.apache.sshd.common.util.io.der.DERParser;
public class OpenSSHEd25519PrivateKeyEntryDecoder extends AbstractPrivateKeyEntryDecoder<EdECPublicKey, EdECPrivateKey> {
public static final OpenSSHEd25519PrivateKeyEntryDecoder INSTANCE = new OpenSSHEd25519PrivateKeyEntryDecoder();
private static final int PK_SIZE = 32;
private static final int SK_SIZE = 32;
private static final int KEYPAIR_SIZE = PK_SIZE + SK_SIZE;
public OpenSSHEd25519PrivateKeyEntryDecoder() {
super(EdECPublicKey.class, EdECPrivateKey.class, Collections.singletonList(KeyPairProvider.SSH_ED25519));
}
@Override
public EdECPrivateKey decodePrivateKey(SessionContext session,
String keyType,
FilePasswordProvider passwordProvider,
InputStream keyData)
throws IOException, GeneralSecurityException {
if (!"ssh-ed25519".equals(keyType)) {
throw new InvalidKeyException("Unsupported key type: " + keyType);
}
// ed25519 bernstein naming: pk .. public key, sk .. secret key
// we expect to find two byte arrays with the following structure (type:size):
// [pk:32], [sk:32,pk:32]
byte[] pk = GenericUtils.EMPTY_BYTE_ARRAY;
byte[] keypair = GenericUtils.EMPTY_BYTE_ARRAY;
try {
pk = KeyEntryResolver.readRLEBytes(keyData, PK_SIZE * 2);
keypair = KeyEntryResolver.readRLEBytes(keyData, KEYPAIR_SIZE * 2);
if (pk.length != PK_SIZE) {
throw new InvalidKeyException(
String.format(Locale.ENGLISH, "Unexpected pk size: %s (expected %s)", pk.length, PK_SIZE));
}
if (keypair.length != KEYPAIR_SIZE) {
throw new InvalidKeyException(
String.format(Locale.ENGLISH, "Unexpected keypair size: %s (expected %s)", keypair.length,
KEYPAIR_SIZE));
}
// verify that the keypair contains the expected pk
// yes, it's stored redundant, this seems to mimic the output structure of the keypair generation interface
if (!Arrays.equals(pk, Arrays.copyOfRange(keypair, SK_SIZE, KEYPAIR_SIZE))) {
throw new InvalidKeyException("Keypair did not contain the public key.");
}
byte[] seed = Arrays.copyOf(keypair, SK_SIZE);
NamedParameterSpec spec = NamedParameterSpec.ED25519;
EdECPrivateKeySpec keySpec = new EdECPrivateKeySpec(spec, seed);
KeyFactory factory = KeyFactory.getInstance("EdDSA");
return EdECPrivateKey.class.cast(factory.generatePrivate(keySpec));
} finally {
// get rid of sensitive data a.s.a.p
Arrays.fill(pk, (byte) 0);
Arrays.fill(keypair, (byte) 0);
}
}
@Override
public String encodePrivateKey(SecureByteArrayOutputStream s,
EdECPrivateKey key,
EdECPublicKey pubKey) throws IOException {
Objects.requireNonNull(key, "No private key provided");
return "ssh-ed25519";
}
@Override
public boolean isPublicKeyRecoverySupported() {
return true;
}
@Override
public EdECPublicKey recoverPublicKey(EdECPrivateKey key) throws GeneralSecurityException {
byte[] pk = key.getEncoded();
boolean xisodd = false;
int lastbyteInt = pk[pk.length - 1];
if ((lastbyteInt & 255) >> 7 == 1) {
xisodd = true;
}
pk[pk.length - 1] &= 127;
BigInteger y = new BigInteger(1, pk);
NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");
EdECPoint ep = new EdECPoint(xisodd, y);
EdECPublicKeySpec publicKeySpec = new EdECPublicKeySpec(paramSpec, ep);
KeyFactory factory = KeyFactory.getInstance("EdDSA");
return EdECPublicKey.class.cast(factory.generatePublic(publicKeySpec));
}
@Override
public EdECPublicKey clonePublicKey(EdECPublicKey key) throws GeneralSecurityException {
if (key == null) {
return null;
} else {
NamedParameterSpec namedParameterSpec = NamedParameterSpec.ED25519;
EdECPoint edECPoint = key.getPoint();
EdECPublicKeySpec spec = new EdECPublicKeySpec(namedParameterSpec, edECPoint);
return generatePublicKey(spec);
}
}
@Override
public EdECPrivateKey clonePrivateKey(EdECPrivateKey key) throws GeneralSecurityException {
if (key == null) {
return null;
} else {
return getPrivateKey(key.getEncoded());
}
}
@Override
public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException {
return KeyPairGenerator.getInstance("Ed25519");
}
@Override
public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException {
return KeyFactory.getInstance("EdDSA");
}
private static EdECPrivateKey getPrivateKey(byte[] keyData) throws GeneralSecurityException {
try (DERParser parser = new DERParser(keyData)) {
ASN1Object obj = parser.readObject();
if (obj == null) {
throw new StreamCorruptedException("Missing key data container");
}
ASN1Type objType = obj.getObjType();
if (objType != ASN1Type.OCTET_STRING) {
throw new StreamCorruptedException("Mismatched key data container type: " + objType);
}
return generatePrivateKey(obj.getValue());
} catch (IOException e) {
throw new GeneralSecurityException(e);
}
}
private static EdECPrivateKey generatePrivateKey(byte[] seed) throws GeneralSecurityException {
NamedParameterSpec spec = NamedParameterSpec.ED25519;
EdECPrivateKeySpec keySpec = new EdECPrivateKeySpec(spec, seed);
KeyFactory factory = KeyFactory.getInstance("EdDSA");
return EdECPrivateKey.class.cast(factory.generatePrivate(keySpec));
}
}

View file

@ -0,0 +1,26 @@
package org.apache.sshd.common.util.security.edec;
import java.util.Map;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.signature.AbstractSignature;
import org.apache.sshd.common.util.ValidateUtils;
public class SignatureEd25519 extends AbstractSignature {
public SignatureEd25519() {
super("NONEwithEdDSA");
}
@Override
public boolean verify(SessionContext session, byte[] sig) throws Exception {
byte[] data = sig;
Map.Entry<String, byte[]> encoding = extractEncodedSignature(data, KeyPairProvider.SSH_ED25519::equalsIgnoreCase);
if (encoding != null) {
String keyType = encoding.getKey();
ValidateUtils.checkTrue(KeyPairProvider.SSH_ED25519.equals(keyType), "Mismatched key type: %s", keyType);
data = encoding.getValue();
}
return doVerify(data);
}
}

View file

@ -1,62 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.common.util.security.xbib;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
import org.apache.sshd.common.session.SessionContext;
import org.xbib.net.security.PrivateKeyReader;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class XbibKeyPairResourceParser implements KeyPairResourceParser {
public static final XbibKeyPairResourceParser INSTANCE = new XbibKeyPairResourceParser();
public XbibKeyPairResourceParser() {
}
@Override
public boolean canExtractKeyPairs(NamedResource resourceKey, List<String> lines) {
return true;
}
@Override
public Collection<KeyPair> loadKeyPairs(SessionContext session,
NamedResource resourceKey,
FilePasswordProvider passwordProvider,
List<String> lines) throws IOException, GeneralSecurityException {
PrivateKeyReader privateKeyReader = new PrivateKeyReader();
InputStream inputStream = new ByteArrayInputStream(String.join("\n", lines).getBytes(StandardCharsets.US_ASCII));
String password = passwordProvider.getPassword(session, resourceKey, 0);
return Collections.singleton(privateKeyReader.generateFrom(inputStream, password));
}
}