Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SSHD-704] Add support for RFC 8731 #177

Merged
merged 2 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ public static BigInteger fromMPIntBytes(byte[] mpInt) {
}

if ((mpInt[0] & 0x80) != 0) {
return new BigInteger(0, mpInt);
return new BigInteger(1, mpInt);
} else {
return new BigInteger(mpInt);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public void init(byte[] v_s, byte[] v_c, byte[] i_s, byte[] i_c) throws Exceptio
log.debug("init({})[{}] Send SSH_MSG_KEXDH_INIT", this, s);
}
Buffer buffer = s.createBuffer(SshConstants.SSH_MSG_KEXDH_INIT, e.length + Integer.SIZE);
buffer.putMPInt(e);
dh.putE(buffer, e);

s.writePacket(buffer);
}
Expand Down Expand Up @@ -167,8 +167,8 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
buffer.putBytes(i_c);
buffer.putBytes(i_s);
buffer.putBytes(k_s);
buffer.putMPInt(getE());
buffer.putMPInt(f);
dh.putE(buffer, getE());
dh.putF(buffer, f);
buffer.putMPInt(k);
hash.update(buffer.array(), 0, buffer.available());
h = hash.digest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public class BaseBuilder<T extends AbstractFactoryManager, S extends BaseBuilder
*/
public static final List<BuiltinDHFactories> DEFAULT_KEX_PREFERENCE = Collections.unmodifiableList(
Arrays.asList(
BuiltinDHFactories.curve25519,
BuiltinDHFactories.curve25519_libssh,
BuiltinDHFactories.curve448,
BuiltinDHFactories.ecdhp521,
BuiltinDHFactories.ecdhp384,
BuiltinDHFactories.ecdhp256,
Expand Down
11 changes: 11 additions & 0 deletions sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractDH.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.buffer.Buffer;

/**
* Base class for the Diffie-Hellman key agreement.
Expand Down Expand Up @@ -63,6 +64,16 @@ public byte[] getE() throws Exception {
return e_array;
}

public void putE(Buffer buffer, byte[] e) {
// RFC 4253, section 8: e and f are encoded as mpints.
buffer.putMPInt(e);
}

public void putF(Buffer buffer, byte[] f) {
// RFC 4253, section 8: e and f are encoded as mpints.
buffer.putMPInt(f);
}

public boolean isSharedSecretAvailable() {
return k_array != null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,54 @@ public ECDH create(Object... params) throws Exception {
public boolean isSupported() {
return ECCurves.nistp521.isSupported();
}
},
/**
* @see <a href="https://www.rfc-editor.org/info/rfc8731">RFC 8731</a>
*/
curve25519(Constants.CURVE25519_SHA256) {
@Override
public XDH create(Object... params) throws Exception {
if (!GenericUtils.isEmpty(params)) {
throw new IllegalArgumentException("No accepted parameters for " + getName());
}
return new XDH(MontgomeryCurve.x25519);
}

@Override
public boolean isSupported() {
return MontgomeryCurve.x25519.isSupported();
}
},
curve25519_libssh(Constants.CURVE25519_SHA256_LIBSSH) {
@Override
public XDH create(Object... params) throws Exception {
if (!GenericUtils.isEmpty(params)) {
throw new IllegalArgumentException("No accepted parameters for " + getName());
}
return new XDH(MontgomeryCurve.x25519);
}

@Override
public boolean isSupported() {
return MontgomeryCurve.x25519.isSupported();
}
},
/**
* @see <a href="https://www.rfc-editor.org/info/rfc8731">RFC 8731</a>
*/
curve448(Constants.CURVE448_SHA512) {
@Override
public XDH create(Object... params) throws Exception {
if (!GenericUtils.isEmpty(params)) {
throw new IllegalArgumentException("No accepted parameters for " + getName());
}
return new XDH(MontgomeryCurve.x448);
}

@Override
public boolean isSupported() {
return MontgomeryCurve.x448.isSupported();
}
};

public static final Set<BuiltinDHFactories> VALUES = Collections.unmodifiableSet(EnumSet.allOf(BuiltinDHFactories.class));
Expand Down Expand Up @@ -417,6 +465,9 @@ public static final class Constants {
public static final String ECDH_SHA2_NISTP256 = "ecdh-sha2-nistp256";
public static final String ECDH_SHA2_NISTP384 = "ecdh-sha2-nistp384";
public static final String ECDH_SHA2_NISTP521 = "ecdh-sha2-nistp521";
public static final String CURVE25519_SHA256 = "curve25519-sha256";
public static final String CURVE25519_SHA256_LIBSSH = "curve25519-sha256@libssh.org";
public static final String CURVE448_SHA512 = "curve448-sha512";

private Constants() {
throw new UnsupportedOperationException("No instance allowed");
Expand Down
15 changes: 15 additions & 0 deletions sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.security.SecurityUtils;

/**
Expand Down Expand Up @@ -100,6 +101,20 @@ public void setF(byte[] f) {
this.f = ECCurves.octetStringToEcPoint(f);
}

@Override
public void putE(Buffer buffer, byte[] e) {
// RFC 5656, section 4: Q_C and Q_S, which take the place of e and f, are written as "strings", i.e., byte
// arrays.
buffer.putBytes(e);
}

@Override
public void putF(Buffer buffer, byte[] f) {
// RFC 5656, section 4: Q_C and Q_S, which take the place of e and f, are written as "strings", i.e., byte
// arrays.
buffer.putBytes(f);
}

@Override
public Digest getHash() throws Exception {
if (curve == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* 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.kex;

import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

import javax.crypto.KeyAgreement;

import org.apache.sshd.common.OptionalFeature;
import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.digest.DigestFactory;
import org.apache.sshd.common.keyprovider.KeySizeIndicator;
import org.apache.sshd.common.util.security.SecurityUtils;

/**
* Provides implementation details for Montgomery curves and their key exchange algorithms Curve25519/X25519 and
* Curve448/X448 specified in RFC 7748 and RFC 8731. Montgomery curves provide improved security and flexibility over
* Weierstrass curves used in ECDH.
*
* @see <a href="https://tools.ietf.org/html/rfc7748">RFC 7748</a>
* @see <a href="https://tools.ietf.org/html/rfc8731">RFC 8731</a>
*/
public enum MontgomeryCurve implements KeySizeIndicator, OptionalFeature {

/**
* The "magic" bytes below are the beginning of a DER encoding of the ASN.1 of the SubjectPublicKeyInfo as specified
* in <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a>, sections 3 and 4.
*
* <pre>
* AlgorithmIdentifier ::= SEQUENCE {
* algorithm OBJECT IDENTIFIER,
* parameters ANY DEFINED BY algorithm OPTIONAL -- absent for these keys
* }
* SubjectPublicKeyInfo ::= SEQUENCE {
* algorithm AlgorithmIdentifier,
* subjectPublicKey BIT STRING
* }
* </pre>
* <p>
* If we take it apart the first one for x25519:
* </p>
*
* <pre>
* 0x30 - SEQUENCE (start of SubjectPublicKeyInfo)
* 0x2a - of 42 bytes
* 0x30 - SEQUENCE (start of AlgorithmIdentifier)
* 0x05 - of 5 bytes
* 0x06 - OID
* 0x03 - of 3 bytes
* 0x2b - 1 3 (encoded as 1*40 + 3 = 43 = 0x2b)
* 0x65 - 101
* 0x6e - 110
* 0x03 - BIT STRING
* 0x21 - of 33 bytes
* 0x00 - NUL byte
* </pre>
* <p>
* If one appends now the 32 public key bytes, the DER encoding for the x25519 public key is complete. The NUL byte
* at the end ensures that the raw key bytes appended are always interpreted as unsigned, even if the most
* significant bit is set.
* </p>
* <p>
* The OID for x25519 is { 1 3 101 110 }, for x448 { 1 3 101 111 }.
* </p>
*/

/**
* X25519 uses Curve25519 and SHA-256 with a 32-byte key size.
*/
x25519("X25519", 32, BuiltinDigests.sha256,
new byte[] { 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00 }),
tomaswolf marked this conversation as resolved.
Show resolved Hide resolved

/**
* X448 uses Curve448 and SHA-512 with a 56-byte key size.
*/
x448("X448", 56, BuiltinDigests.sha512,
new byte[] { 0x30, 0x42, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6f, 0x03, 0x39, 0x00 });

private final String algorithm;
private final int keySize;
private final boolean supported;
private final DigestFactory digestFactory;
private final KeyPairGenerator keyPairGenerator;
private final KeyFactory keyFactory;
private final byte[] encodedPublicKeyPrefix;

MontgomeryCurve(String algorithm, int keySize, DigestFactory digestFactory, byte[] encodedPublicKeyPrefix) {
this.algorithm = algorithm;
this.keySize = keySize;
this.digestFactory = digestFactory;
this.encodedPublicKeyPrefix = encodedPublicKeyPrefix;
boolean supported;
KeyPairGenerator generator = null;
KeyFactory factory = null;
try {
SecurityUtils.getKeyAgreement(algorithm);
generator = SecurityUtils.getKeyPairGenerator(algorithm);
factory = SecurityUtils.getKeyFactory(algorithm);
supported = true;
} catch (GeneralSecurityException ignored) {
supported = false;
}
this.supported = supported && digestFactory.isSupported();
keyPairGenerator = generator;
keyFactory = factory;
}

public String getAlgorithm() {
return algorithm;
}

@Override
public int getKeySize() {
return keySize;
}

@Override
public boolean isSupported() {
return supported;
}

public KeyAgreement createKeyAgreement() throws GeneralSecurityException {
return SecurityUtils.getKeyAgreement(algorithm);
}

public Digest createDigest() {
return digestFactory.create();
}

public KeyPair generateKeyPair() {
return keyPairGenerator.generateKeyPair();
}

public byte[] encode(PublicKey key) throws InvalidKeyException {
// Per the ASN.1 of SubjectPublicKeyInfo, the key must be the last keySize bytes of the X.509 encoding
byte[] subjectPublicKeyInfo = key.getEncoded();
byte[] result = Arrays.copyOfRange(subjectPublicKeyInfo, subjectPublicKeyInfo.length - getKeySize(),
subjectPublicKeyInfo.length);
return result;
}

public PublicKey decode(byte[] key) throws InvalidKeySpecException {
int size = getKeySize();
int offset = key.length - size;
// We're lenient here and accept a key prefixed by a zero byte.
if (offset < 0 || offset > 1) {
throw new InvalidKeySpecException("Provided key has wrong length (" + key.length + " bytes) for " + getAlgorithm());
} else if (offset == 1) {
if (key[0] != 0) {
throw new InvalidKeySpecException(
"Provided key for " + getAlgorithm()
+ " has extra byte, but it's non-zero: 0x"
+ Integer.toHexString(key[0] & 0xFF));
}
}
// Ideally, we'd just parse the key as a BigInteger and then create a XECPublicKeySpec in Java 11
// BouncyCastle supports a separate API, but we can use the generic X.509 encoding scheme supported by both
byte[] encoded = Arrays.copyOf(encodedPublicKeyPrefix, encodedPublicKeyPrefix.length + size);
System.arraycopy(key, offset, encoded, encodedPublicKeyPrefix.length, size);
return keyFactory.generatePublic(new X509EncodedKeySpec(encoded));
}
}
Loading