Skip to content

Commit 9886abb

Browse files
Added support for ssh-ed25519 to jagged-ssh
1 parent 6b9315c commit 9886abb

File tree

51 files changed

+3089
-24
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3089
-24
lines changed

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Jagged supports streaming encryption and decryption using standard recipient typ
6666
- [X25519](https://github.com/C2SP/C2SP/blob/main/age.md#the-x25519-recipient-type) recipients and identities
6767
- [scrypt](https://github.com/C2SP/C2SP/blob/main/age.md#the-scrypt-recipient-type) recipients and identities
6868
- [ssh-rsa](https://github.com/FiloSottile/age/blob/main/README.md#ssh-keys) recipients and identities
69+
- [ssh-ed25519](https://github.com/FiloSottile/age/blob/main/README.md#ssh-keys) recipients and identities
6970

7071
# Specifications
7172

@@ -120,6 +121,21 @@ a File Key.
120121

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

124+
The ssh-ed25519 and ssh-rsa types support reading private key pairs formatted using OpenSSH Private Key Version 1.
125+
126+
- [OpenSSH PROTOCOL.key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
127+
128+
The ssh-ed25519 type uses Curve25519 for Elliptic Curve Diffie-Hellman shared secret key exchanges based on computing
129+
equivalent values from keys described in the Edwards-curve Digital Signature Algorithm edwards25519.
130+
131+
- [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) Edwards-Curve Digital Signature Algorithm
132+
133+
The ssh-ed25519 type reads SSH public keys encoded according to the SSH protocol.
134+
135+
- [RFC 8709](https://www.rfc-editor.org/rfc/rfc8709) Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol
136+
137+
The ssh-ed25519 type encrypts a File Key with ChaCha20-Poly1305.
138+
123139
The ssh-rsa type encrypts a File Key with RSA-OAEP.
124140

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

258+
The `SshEd25519RecipientStanzaReaderFactory` creates instances of `RecipientStanzaReader` using an
259+
[OpenSSH Version 1 Private Key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key).
260+
261+
The `SshEd25519RecipientStanzaWriterFactory` creates instances of `RecipientStanzaWriter` using an SSH Ed25519 public
262+
key encoded according to [RFC 8709 Section 4](https://www.rfc-editor.org/rfc/rfc8709#name-public-key-format).
263+
242264
The `SshRsaRecipientStanzaReaderFactory` creates instances of `RecipientStanzaReader` using an RSA private key or an
243265
[OpenSSH Version 1 Private Key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key).
244266

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

269+
The SSH Ed25519 implementation uses Elliptic Curve Diffie-Hellman with Curve25519 as defined in
270+
[RFC 7748 Section 6.1](https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1). As integrated in the age reference
271+
implementation, the SSH Ed25519 implementation converts the public key coordinate from the twisted Edwards curve to the
272+
corresponding coordinate on the Montgomery curve according to the birational maps described in
273+
[RFC 7748 Section 4.1](https://www.rfc-editor.org/rfc/rfc7748#section-4.1). The implementation converts the Ed25519
274+
private key seed to the corresponding X25519 private key using the first 32 bytes of an `SHA-512` hash of the seed.
275+
The SSH Ed25519 implementation uses ChaCha20-Poly1305 for encrypting and decrypting File Keys.
276+
247277
The SSH RSA implementation uses Optimal Asymmetric Encryption Padding as defined in
248278
[RFC 8017 Section 7.1](https://www.rfc-editor.org/rfc/rfc8017#section-7.1). Following the age implementation, RSA OAEP
249279
cipher operations use `SHA-256` as the hash algorithm with the mask generation function.

jagged-framework/src/main/java/com/exceptionfactory/jagged/framework/crypto/CryptographicAlgorithmKey.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ class CryptographicAlgorithmKey implements SecretKey {
4040
* Cryptographic Algorithm Key constructor with required symmetric key
4141
*
4242
* @param key Symmetric Key
43-
* @param cryptographicKeyType Cryptographic Key Type
43+
* @param cryptographicKeyDescription Cryptographic Key Description
4444
* @param cryptographicAlgorithm Cryptographic Algorithm
4545
*/
46-
CryptographicAlgorithmKey(final byte[] key, final CryptographicKeyType cryptographicKeyType, final CryptographicAlgorithm cryptographicAlgorithm) {
47-
this(getValidatedKey(key, cryptographicKeyType), cryptographicAlgorithm);
46+
CryptographicAlgorithmKey(final byte[] key, final CryptographicKeyDescription cryptographicKeyDescription, final CryptographicAlgorithm cryptographicAlgorithm) {
47+
this(getValidatedKey(key, cryptographicKeyDescription), cryptographicAlgorithm);
4848
}
4949

5050
private CryptographicAlgorithmKey(final byte[] validatedKey, final CryptographicAlgorithm cryptographicAlgorithm) {
@@ -101,10 +101,10 @@ public boolean isDestroyed() {
101101
return destroyed.get();
102102
}
103103

104-
private static byte[] getValidatedKey(final byte[] key, final CryptographicKeyType cryptographicKeyType) {
104+
private static byte[] getValidatedKey(final byte[] key, final CryptographicKeyDescription cryptographicKeyDescription) {
105105
Objects.requireNonNull(key, "Symmetric Key required");
106-
Objects.requireNonNull(cryptographicKeyType, "Cryptographic Key Type required");
107-
final int cryptographicKeyLength = cryptographicKeyType.getKeyLength();
106+
Objects.requireNonNull(cryptographicKeyDescription, "Cryptographic Key Description required");
107+
final int cryptographicKeyLength = cryptographicKeyDescription.getKeyLength();
108108
if (cryptographicKeyLength == key.length) {
109109
return key;
110110
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 Jagged Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.exceptionfactory.jagged.framework.crypto;
17+
18+
/**
19+
* Abstraction for describing Cryptographic Key properties
20+
*/
21+
public interface CryptographicKeyDescription {
22+
/**
23+
* Get key length in bytes
24+
*
25+
* @return Key length in bytes
26+
*/
27+
int getKeyLength();
28+
}

jagged-framework/src/main/java/com/exceptionfactory/jagged/framework/crypto/CryptographicKeyType.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Cryptographic Key Type references for construction and validation
2020
*/
21-
enum CryptographicKeyType {
21+
enum CryptographicKeyType implements CryptographicKeyDescription {
2222
/** Extracted intermediate key for subsequent expansion */
2323
EXTRACTED_KEY(32),
2424

@@ -51,7 +51,8 @@ enum CryptographicKeyType {
5151
*
5252
* @return Key length in bytes
5353
*/
54-
int getKeyLength() {
54+
@Override
55+
public int getKeyLength() {
5556
return keyLength;
5657
}
5758
}

jagged-framework/src/main/java/com/exceptionfactory/jagged/framework/crypto/MacKey.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ public class MacKey extends CryptographicAlgorithmKey {
2323
* Message Authentication Code Key constructor with required symmetric key
2424
*
2525
* @param key Symmetric Key with byte length based on Cryptographic Key Type
26-
* @param cryptographicKeyType Cryptographic Key Type
26+
* @param cryptographicKeyDescription Cryptographic Key Description
2727
*/
28-
MacKey(final byte[] key, final CryptographicKeyType cryptographicKeyType) {
29-
super(key, cryptographicKeyType, CryptographicAlgorithm.HMACSHA256);
28+
public MacKey(final byte[] key, final CryptographicKeyDescription cryptographicKeyDescription) {
29+
super(key, cryptographicKeyDescription, CryptographicAlgorithm.HMACSHA256);
3030
}
3131
}

jagged-ssh/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
<groupId>com.exceptionfactory.jagged</groupId>
2121
<artifactId>jagged-api</artifactId>
2222
</dependency>
23+
<dependency>
24+
<groupId>com.exceptionfactory.jagged</groupId>
25+
<artifactId>jagged-framework</artifactId>
26+
</dependency>
2327
<dependency>
2428
<groupId>org.junit.jupiter</groupId>
2529
<artifactId>junit-jupiter-api</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023 Jagged Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.exceptionfactory.jagged.ssh;
17+
18+
import com.exceptionfactory.jagged.framework.crypto.SharedSecretKey;
19+
20+
import java.security.GeneralSecurityException;
21+
import java.security.PrivateKey;
22+
import java.security.PublicKey;
23+
24+
/**
25+
* Abstraction for converting Ed25519 keys to X25519 keys
26+
*/
27+
interface Ed25519KeyConverter {
28+
/**
29+
* Get X25519 Private Key from Ed25519 Private Key using first 32 bytes of SHA-512 digested key
30+
*
31+
* @param ed25519PrivateKey Ed25519 private key
32+
* @return X25519 Private Key
33+
* @throws GeneralSecurityException Thrown on failure to convert private key
34+
*/
35+
PrivateKey getPrivateKey(Ed25519PrivateKey ed25519PrivateKey) throws GeneralSecurityException;
36+
37+
/**
38+
* Get X25519 Private Key from SSH Ed25519 derived key
39+
*
40+
* @param derivedKey SSH Ed25519 derived key
41+
* @return X25519 Private Key
42+
* @throws GeneralSecurityException Thrown on failure to convert private key
43+
*/
44+
PrivateKey getPrivateKey(SshEd25519DerivedKey derivedKey) throws GeneralSecurityException;
45+
46+
/**
47+
* Get X25519 Public Key from Ed25519 Public Key computed using birational mapping described in RFC 7748 Section 4.1
48+
*
49+
* @param ed25519PublicKey Ed25519 public key
50+
* @return X25519 Public Key
51+
* @throws GeneralSecurityException Thrown on failure to convert public key
52+
*/
53+
PublicKey getPublicKey(Ed25519PublicKey ed25519PublicKey) throws GeneralSecurityException;
54+
55+
/**
56+
* Get X25519 Public Key from computed Shared Secret Key
57+
*
58+
* @param sharedSecretKey Computed shared secret key
59+
* @return X25519 Public Key
60+
* @throws GeneralSecurityException Thrown on key processing failures
61+
*/
62+
PublicKey getPublicKey(SharedSecretKey sharedSecretKey) throws GeneralSecurityException;
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2023 Jagged Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.exceptionfactory.jagged.ssh;
17+
18+
/**
19+
* Ed25519 Key indicator fields
20+
*/
21+
enum Ed25519KeyIndicator {
22+
/** Algorithm */
23+
KEY_ALGORITHM("Ed25519"),
24+
25+
/** Format */
26+
KEY_FORMAT("RAW");
27+
28+
private final String indicator;
29+
30+
Ed25519KeyIndicator(final String indicator) {
31+
this.indicator = indicator;
32+
}
33+
34+
String getIndicator() {
35+
return indicator;
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2023 Jagged Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.exceptionfactory.jagged.ssh;
17+
18+
import java.security.PrivateKey;
19+
import java.util.Arrays;
20+
import java.util.Objects;
21+
import java.util.concurrent.atomic.AtomicBoolean;
22+
23+
/**
24+
* Ed25519 Private Key containing raw key bytes
25+
*/
26+
class Ed25519PrivateKey implements PrivateKey {
27+
private static final byte ZERO = 0;
28+
29+
private final AtomicBoolean destroyed = new AtomicBoolean();
30+
31+
private final byte[] encoded;
32+
33+
/**
34+
* Ed25519 Private Key constructor with raw bytes containing private key seed
35+
*
36+
* @param encoded private key seed of 32 bytes
37+
*/
38+
Ed25519PrivateKey(final byte[] encoded) {
39+
this.encoded = Objects.requireNonNull(encoded, "Encoded Key required");
40+
}
41+
42+
/**
43+
* Get algorithm describes the type of key
44+
*
45+
* @return Algorithm is Ed25519
46+
*/
47+
@Override
48+
public String getAlgorithm() {
49+
return Ed25519KeyIndicator.KEY_ALGORITHM.getIndicator();
50+
}
51+
52+
/**
53+
* Get format describes the encoded content bytes
54+
*
55+
* @return Encoded key format is RAW
56+
*/
57+
@Override
58+
public String getFormat() {
59+
return Ed25519KeyIndicator.KEY_FORMAT.getIndicator();
60+
}
61+
62+
/**
63+
* Get encoded key bytes consisting of private key seed
64+
*
65+
* @return encoded private key array of 32 bytes
66+
*/
67+
@Override
68+
public byte[] getEncoded() {
69+
return encoded.clone();
70+
}
71+
72+
/**
73+
* Get string representation of key algorithm
74+
*
75+
* @return Key algorithm
76+
*/
77+
@Override
78+
public String toString() {
79+
return getAlgorithm();
80+
}
81+
82+
/**
83+
* Destroy Key so that it cannot be used for subsequent operations
84+
*/
85+
@Override
86+
public void destroy() {
87+
Arrays.fill(encoded, ZERO);
88+
destroyed.set(true);
89+
}
90+
91+
/**
92+
* Return destroyed status
93+
*
94+
* @return Key destroyed status
95+
*/
96+
@Override
97+
public boolean isDestroyed() {
98+
return destroyed.get();
99+
}
100+
}

0 commit comments

Comments
 (0)