Skip to content

Commit

Permalink
Added support for ssh-ed25519 to jagged-ssh (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
exceptionfactory authored Jul 11, 2024
1 parent 6b9315c commit 0e3e6e4
Show file tree
Hide file tree
Showing 51 changed files with 3,089 additions and 24 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Jagged supports streaming encryption and decryption using standard recipient typ
- [X25519](https://github.com/C2SP/C2SP/blob/main/age.md#the-x25519-recipient-type) recipients and identities
- [scrypt](https://github.com/C2SP/C2SP/blob/main/age.md#the-scrypt-recipient-type) recipients and identities
- [ssh-rsa](https://github.com/FiloSottile/age/blob/main/README.md#ssh-keys) recipients and identities
- [ssh-ed25519](https://github.com/FiloSottile/age/blob/main/README.md#ssh-keys) recipients and identities

# Specifications

Expand Down Expand Up @@ -120,6 +121,21 @@ a File Key.

The scrypt type encrypts a File Key with ChaCha20-Poly1305.

The ssh-ed25519 and ssh-rsa types support reading private key pairs formatted using OpenSSH Private Key Version 1.

- [OpenSSH PROTOCOL.key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)

The ssh-ed25519 type uses Curve25519 for Elliptic Curve Diffie-Hellman shared secret key exchanges based on computing
equivalent values from keys described in the Edwards-curve Digital Signature Algorithm edwards25519.

- [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) Edwards-Curve Digital Signature Algorithm

The ssh-ed25519 type reads SSH public keys encoded according to the SSH protocol.

- [RFC 8709](https://www.rfc-editor.org/rfc/rfc8709) Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol

The ssh-ed25519 type encrypts a File Key with ChaCha20-Poly1305.

The ssh-rsa type encrypts a File Key with RSA-OAEP.

- [RFC 8017](https://www.rfc-editor.org/rfc/rfc8017) PKCS #1: RSA Cryptography Specifications Version 2.2
Expand Down Expand Up @@ -239,11 +255,25 @@ The `jagged-ssh` module supports encryption and decryption using public and priv
implementation is compatible with the [agessh](https://pkg.go.dev/filippo.io/age/agessh) package, which defines
recipient stanzas with an algorithm and an encoded fingerprint of the public key.

The `SshEd25519RecipientStanzaReaderFactory` creates instances of `RecipientStanzaReader` using an
[OpenSSH Version 1 Private Key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key).

The `SshEd25519RecipientStanzaWriterFactory` creates instances of `RecipientStanzaWriter` using an SSH Ed25519 public
key encoded according to [RFC 8709 Section 4](https://www.rfc-editor.org/rfc/rfc8709#name-public-key-format).

The `SshRsaRecipientStanzaReaderFactory` creates instances of `RecipientStanzaReader` using an RSA private key or an
[OpenSSH Version 1 Private Key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key).

The `SshRsaRecipientStanzaWriterFactory` creates instances of `RecipientStanzaWriter` using an RSA public key.

The SSH Ed25519 implementation uses Elliptic Curve Diffie-Hellman with Curve25519 as defined in
[RFC 7748 Section 6.1](https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1). As integrated in the age reference
implementation, the SSH Ed25519 implementation converts the public key coordinate from the twisted Edwards curve to the
corresponding coordinate on the Montgomery curve according to the birational maps described in
[RFC 7748 Section 4.1](https://www.rfc-editor.org/rfc/rfc7748#section-4.1). The implementation converts the Ed25519
private key seed to the corresponding X25519 private key using the first 32 bytes of an `SHA-512` hash of the seed.
The SSH Ed25519 implementation uses ChaCha20-Poly1305 for encrypting and decrypting File Keys.

The SSH RSA implementation uses Optimal Asymmetric Encryption Padding as defined in
[RFC 8017 Section 7.1](https://www.rfc-editor.org/rfc/rfc8017#section-7.1). Following the age implementation, RSA OAEP
cipher operations use `SHA-256` as the hash algorithm with the mask generation function.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ class CryptographicAlgorithmKey implements SecretKey {
* Cryptographic Algorithm Key constructor with required symmetric key
*
* @param key Symmetric Key
* @param cryptographicKeyType Cryptographic Key Type
* @param cryptographicKeyDescription Cryptographic Key Description
* @param cryptographicAlgorithm Cryptographic Algorithm
*/
CryptographicAlgorithmKey(final byte[] key, final CryptographicKeyType cryptographicKeyType, final CryptographicAlgorithm cryptographicAlgorithm) {
this(getValidatedKey(key, cryptographicKeyType), cryptographicAlgorithm);
CryptographicAlgorithmKey(final byte[] key, final CryptographicKeyDescription cryptographicKeyDescription, final CryptographicAlgorithm cryptographicAlgorithm) {
this(getValidatedKey(key, cryptographicKeyDescription), cryptographicAlgorithm);
}

private CryptographicAlgorithmKey(final byte[] validatedKey, final CryptographicAlgorithm cryptographicAlgorithm) {
Expand Down Expand Up @@ -101,10 +101,10 @@ public boolean isDestroyed() {
return destroyed.get();
}

private static byte[] getValidatedKey(final byte[] key, final CryptographicKeyType cryptographicKeyType) {
private static byte[] getValidatedKey(final byte[] key, final CryptographicKeyDescription cryptographicKeyDescription) {
Objects.requireNonNull(key, "Symmetric Key required");
Objects.requireNonNull(cryptographicKeyType, "Cryptographic Key Type required");
final int cryptographicKeyLength = cryptographicKeyType.getKeyLength();
Objects.requireNonNull(cryptographicKeyDescription, "Cryptographic Key Description required");
final int cryptographicKeyLength = cryptographicKeyDescription.getKeyLength();
if (cryptographicKeyLength == key.length) {
return key;
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2023 Jagged Contributors
*
* Licensed 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 com.exceptionfactory.jagged.framework.crypto;

/**
* Abstraction for describing Cryptographic Key properties
*/
public interface CryptographicKeyDescription {
/**
* Get key length in bytes
*
* @return Key length in bytes
*/
int getKeyLength();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/**
* Cryptographic Key Type references for construction and validation
*/
enum CryptographicKeyType {
enum CryptographicKeyType implements CryptographicKeyDescription {
/** Extracted intermediate key for subsequent expansion */
EXTRACTED_KEY(32),

Expand Down Expand Up @@ -51,7 +51,8 @@ enum CryptographicKeyType {
*
* @return Key length in bytes
*/
int getKeyLength() {
@Override
public int getKeyLength() {
return keyLength;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public class MacKey extends CryptographicAlgorithmKey {
* Message Authentication Code Key constructor with required symmetric key
*
* @param key Symmetric Key with byte length based on Cryptographic Key Type
* @param cryptographicKeyType Cryptographic Key Type
* @param cryptographicKeyDescription Cryptographic Key Description
*/
MacKey(final byte[] key, final CryptographicKeyType cryptographicKeyType) {
super(key, cryptographicKeyType, CryptographicAlgorithm.HMACSHA256);
public MacKey(final byte[] key, final CryptographicKeyDescription cryptographicKeyDescription) {
super(key, cryptographicKeyDescription, CryptographicAlgorithm.HMACSHA256);
}
}
4 changes: 4 additions & 0 deletions jagged-ssh/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<groupId>com.exceptionfactory.jagged</groupId>
<artifactId>jagged-api</artifactId>
</dependency>
<dependency>
<groupId>com.exceptionfactory.jagged</groupId>
<artifactId>jagged-framework</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2023 Jagged Contributors
*
* Licensed 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 com.exceptionfactory.jagged.ssh;

import com.exceptionfactory.jagged.framework.crypto.SharedSecretKey;

import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
* Abstraction for converting Ed25519 keys to X25519 keys
*/
interface Ed25519KeyConverter {
/**
* Get X25519 Private Key from Ed25519 Private Key using first 32 bytes of SHA-512 digested key
*
* @param ed25519PrivateKey Ed25519 private key
* @return X25519 Private Key
* @throws GeneralSecurityException Thrown on failure to convert private key
*/
PrivateKey getPrivateKey(Ed25519PrivateKey ed25519PrivateKey) throws GeneralSecurityException;

/**
* Get X25519 Private Key from SSH Ed25519 derived key
*
* @param derivedKey SSH Ed25519 derived key
* @return X25519 Private Key
* @throws GeneralSecurityException Thrown on failure to convert private key
*/
PrivateKey getPrivateKey(SshEd25519DerivedKey derivedKey) throws GeneralSecurityException;

/**
* Get X25519 Public Key from Ed25519 Public Key computed using birational mapping described in RFC 7748 Section 4.1
*
* @param ed25519PublicKey Ed25519 public key
* @return X25519 Public Key
* @throws GeneralSecurityException Thrown on failure to convert public key
*/
PublicKey getPublicKey(Ed25519PublicKey ed25519PublicKey) throws GeneralSecurityException;

/**
* Get X25519 Public Key from computed Shared Secret Key
*
* @param sharedSecretKey Computed shared secret key
* @return X25519 Public Key
* @throws GeneralSecurityException Thrown on key processing failures
*/
PublicKey getPublicKey(SharedSecretKey sharedSecretKey) throws GeneralSecurityException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 Jagged Contributors
*
* Licensed 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 com.exceptionfactory.jagged.ssh;

/**
* Ed25519 Key indicator fields
*/
enum Ed25519KeyIndicator {
/** Algorithm */
KEY_ALGORITHM("Ed25519"),

/** Format */
KEY_FORMAT("RAW");

private final String indicator;

Ed25519KeyIndicator(final String indicator) {
this.indicator = indicator;
}

String getIndicator() {
return indicator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2023 Jagged Contributors
*
* Licensed 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 com.exceptionfactory.jagged.ssh;

import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* Ed25519 Private Key containing raw key bytes
*/
class Ed25519PrivateKey implements PrivateKey {
private static final byte ZERO = 0;

private final AtomicBoolean destroyed = new AtomicBoolean();

private final byte[] encoded;

/**
* Ed25519 Private Key constructor with raw bytes containing private key seed
*
* @param encoded private key seed of 32 bytes
*/
Ed25519PrivateKey(final byte[] encoded) {
this.encoded = Objects.requireNonNull(encoded, "Encoded Key required");
}

/**
* Get algorithm describes the type of key
*
* @return Algorithm is Ed25519
*/
@Override
public String getAlgorithm() {
return Ed25519KeyIndicator.KEY_ALGORITHM.getIndicator();
}

/**
* Get format describes the encoded content bytes
*
* @return Encoded key format is RAW
*/
@Override
public String getFormat() {
return Ed25519KeyIndicator.KEY_FORMAT.getIndicator();
}

/**
* Get encoded key bytes consisting of private key seed
*
* @return encoded private key array of 32 bytes
*/
@Override
public byte[] getEncoded() {
return encoded.clone();
}

/**
* Get string representation of key algorithm
*
* @return Key algorithm
*/
@Override
public String toString() {
return getAlgorithm();
}

/**
* Destroy Key so that it cannot be used for subsequent operations
*/
@Override
public void destroy() {
Arrays.fill(encoded, ZERO);
destroyed.set(true);
}

/**
* Return destroyed status
*
* @return Key destroyed status
*/
@Override
public boolean isDestroyed() {
return destroyed.get();
}
}
Loading

0 comments on commit 0e3e6e4

Please sign in to comment.