diff --git a/build.gradle b/build.gradle index 6bab582..21c4503 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,8 @@ repositories { } dependencies { - implementation 'org.bouncycastle:bcprov-lts8on:2.73.7' + implementation 'org.jspecify:jspecify:1.0.0' + implementation 'org.bouncycastle:bcprov-lts8on:2.73.9' testImplementation 'org.junit.jupiter:junit-jupiter:5.7.0' testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.12.3' @@ -156,4 +157,4 @@ if (isRelease) { sign configurations.archives sign publishing.publications.mavenJava } -} \ No newline at end of file +} diff --git a/src/main/java/io/nats/nkey/KeyWrapper.java b/src/main/java/io/nats/nkey/DefaultSecurityProviderFactory.java similarity index 60% rename from src/main/java/io/nats/nkey/KeyWrapper.java rename to src/main/java/io/nats/nkey/DefaultSecurityProviderFactory.java index a5bae8e..8280e16 100644 --- a/src/main/java/io/nats/nkey/KeyWrapper.java +++ b/src/main/java/io/nats/nkey/DefaultSecurityProviderFactory.java @@ -13,17 +13,16 @@ package io.nats.nkey; -import java.security.Key; +import org.bouncycastle.jce.provider.BouncyCastleProvider; -abstract class KeyWrapper implements Key { +import java.security.Provider; - @Override - public String getAlgorithm() { - return "EdDSA"; - } - - @Override - public String getFormat() { - return "PKCS#8"; +/** + * Wraps construction of {@link BouncyCastleProvider} class to defer loading of the class. + * That allows users to exclude the BouncyCastle dependency if they want to use another provider. + */ +class DefaultSecurityProviderFactory { + static Provider getProvider() { + return new BouncyCastleProvider(); } } diff --git a/src/main/java/io/nats/nkey/KeyCodec.java b/src/main/java/io/nats/nkey/KeyCodec.java new file mode 100644 index 0000000..2215f7a --- /dev/null +++ b/src/main/java/io/nats/nkey/KeyCodec.java @@ -0,0 +1,96 @@ +// Copyright 2025 The NATS Authors +// 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 io.nats.nkey; + +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +class KeyCodec { + // This value can be obtained on Java 15+ with + // KeyPairGenerator.getInstance("Ed25519").generateKeyPair().getPrivate().getEncoded() + // which returns this + private key bytes. + // 48 - sequence tag + // 46 - length + // 2 - integer tag + // 1 - length + // 0 - version - PKCS#8v1 + // 48 - sequence tag + // 5 - length + // 6 - OID tag + // 3 - length + // 43 - 1st byte of Ed25519 OID - "1.3.101.112" + // 101 - 2nd byte + // 112 - 3rd byte + // 4 - octet string tag + // 34 - length + // 4 - octet string tag + // 32 - length + private static final byte[] PRIVATE_KEY_PREFIX = new byte[]{48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32}; + // This value can be obtained on Java 15+ with + // KeyPairGenerator.getInstance("Ed25519").generateKeyPair().getPublic().getEncoded() + // which returns this + public key bytes. + // 48 - sequence tag + // 42 - length + // 48 - sequence tag + // 5 - length + // 6 - OID tag + // 3 - length + // 43 - 1st byte of Ed25519 OID - "1.3.101.112" + // 101 - 2nd byte + // 112 - 3rd byte + // 3 - bit string tag + // 33 - length + // 0 - number of unused bits in final byte + private static final byte[] PUBLIC_KEY_PREFIX = new byte[]{48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0}; + + static byte[] publicKeyToPubBytes(PublicKey key) { + if (!"Ed25519".equals(key.getAlgorithm())) { + throw new IllegalArgumentException("Only Ed25519 keys are supported"); + } + if (!"X.509".equals(key.getFormat())) { + throw new IllegalArgumentException("Only X509 encoded keys are supported"); + } + + byte[] encoded = key.getEncoded(); + if (encoded.length != PUBLIC_KEY_PREFIX.length + 32) { + throw new IllegalArgumentException("Unsupported Ed25519 public key encoding"); + } + for (int i = 0; i < PUBLIC_KEY_PREFIX.length; i++) { + if (encoded[i] != PUBLIC_KEY_PREFIX[i]) { + throw new IllegalArgumentException("Unsupported Ed25519 public key encoding"); + } + } + + return Arrays.copyOfRange(encoded, PUBLIC_KEY_PREFIX.length, encoded.length); + } + + static PKCS8EncodedKeySpec seedBytesToKeySpec(byte[] seedBytes) { + byte[] pkcs8Bytes = concat(PRIVATE_KEY_PREFIX, seedBytes); + return new PKCS8EncodedKeySpec(pkcs8Bytes); + } + + static X509EncodedKeySpec pubBytesToKeySpec(byte[] pubBytes) { + byte[] x509Bytes = concat(PUBLIC_KEY_PREFIX, pubBytes); + return new X509EncodedKeySpec(x509Bytes); + } + + private static byte[] concat(byte[] left, byte[] right) { + byte[] result = new byte[left.length + right.length]; + System.arraycopy(left, 0, result, 0, left.length); + System.arraycopy(right, 0, result, left.length, right.length); + return result; + } +} diff --git a/src/main/java/io/nats/nkey/NKey.java b/src/main/java/io/nats/nkey/NKey.java index 4f5e8d3..a50d765 100644 --- a/src/main/java/io/nats/nkey/NKey.java +++ b/src/main/java/io/nats/nkey/NKey.java @@ -13,20 +13,23 @@ package io.nats.nkey; -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; -import org.bouncycastle.crypto.signers.Ed25519Signer; +import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; import static io.nats.nkey.NKeyConstants.*; import static io.nats.nkey.NKeyUtils.*; +@NullMarked public class NKey { private static boolean notValidPublicPrefixByte(int prefix) { @@ -117,7 +120,7 @@ static byte[] decode(char[] src) { return dataBytes; } - static byte[] decode(NKeyType expectedType, char[] src) { + static byte @Nullable [] decode(NKeyType expectedType, char[] src) { byte[] raw = decode(src); byte[] dataBytes = Arrays.copyOfRange(raw, 1, raw.length); NKeyType type = NKeyType.fromPrefix(raw[0] & 0xFF); @@ -148,7 +151,20 @@ static NKeyDecodedSeed decodeSeed(char[] seed) { return new NKeyDecodedSeed(b2, dataBytes); } - private static NKey createPair(NKeyType type, SecureRandom random) + private static @Nullable Provider getSecurityProvider() { + String property = System.getProperty(SECURITY_PROVIDER_PROPERTY); + if (property == null) { + // Instantiating the BouncyCastle provider to maintain backwards compatibility + return DefaultSecurityProviderFactory.getProvider(); + } + if (property.isEmpty()) { + // Use whatever is configured as the default in the JVM + return null; + } + return Security.getProvider(property); + } + + private static NKey createPair(NKeyType type, @Nullable SecureRandom random) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { byte[] seed = new byte[ED25519_SEED_SIZE]; if (random == null) { @@ -160,18 +176,42 @@ private static NKey createPair(NKeyType type, SecureRandom random) return createPair(type, seed); } - private static NKey createPair(NKeyType type, byte[] seed) throws IOException { - Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(seed); - Ed25519PublicKeyParameters publicKey = privateKey.generatePublicKey(); - - byte[] pubBytes = publicKey.getEncoded(); + private static NKey createPair(NKeyType type, byte[] seed) throws IOException, NoSuchAlgorithmException { + Provider securityProvider = getSecurityProvider(); + byte[] pubBytes = seedToPubBytes(seed, securityProvider); byte[] bytes = new byte[pubBytes.length + seed.length]; System.arraycopy(seed, 0, bytes, 0, seed.length); System.arraycopy(pubBytes, 0, bytes, seed.length, pubBytes.length); char[] encoded = encodeSeed(type, bytes); - return new NKey(type, null, encoded); + return new NKey(type, null, encoded, securityProvider); + } + + private static byte[] seedToPubBytes(byte[] seed, @Nullable Provider securityProvider) throws NoSuchAlgorithmException { + KeyFactory keyFactory = getKeyFactoryInstance(securityProvider); + PublicKey publicKey; + try { + PKCS8EncodedKeySpec privateKeySpec = KeyCodec.seedBytesToKeySpec(seed); + // This cast restricts the securityProvider to BouncyCastle (regular or FIPS edition) + EdDSAPrivateKey privateKey = (EdDSAPrivateKey) keyFactory.generatePrivate(privateKeySpec); + publicKey = privateKey.getPublicKey(); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException("Invalid NKey seed", e); + } + return KeyCodec.publicKeyToPubBytes(publicKey); + } + + private static Signature getSignatureInstance(@Nullable Provider securityProvider) throws NoSuchAlgorithmException { + return securityProvider == null + ? Signature.getInstance(CRYPTO_ALGORITHM) + : Signature.getInstance(CRYPTO_ALGORITHM, securityProvider); + } + + private static KeyFactory getKeyFactoryInstance(@Nullable Provider securityProvider) throws NoSuchAlgorithmException { + return securityProvider == null + ? KeyFactory.getInstance(CRYPTO_ALGORITHM) + : KeyFactory.getInstance(CRYPTO_ALGORITHM, securityProvider); } /** @@ -184,7 +224,7 @@ private static NKey createPair(NKeyType type, byte[] seed) throws IOException { * @throws NoSuchProviderException if the default secure random cannot be created * @throws NoSuchAlgorithmException if the default secure random cannot be created */ - public static NKey createAccount(SecureRandom random) + public static NKey createAccount(@Nullable SecureRandom random) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { return createPair(NKeyType.ACCOUNT, random); } @@ -199,7 +239,7 @@ public static NKey createAccount(SecureRandom random) * @throws NoSuchProviderException if the default secure random cannot be created * @throws NoSuchAlgorithmException if the default secure random cannot be created */ - public static NKey createCluster(SecureRandom random) + public static NKey createCluster(@Nullable SecureRandom random) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { return createPair(NKeyType.CLUSTER, random); } @@ -214,7 +254,7 @@ public static NKey createCluster(SecureRandom random) * @throws NoSuchProviderException if the default secure random cannot be created * @throws NoSuchAlgorithmException if the default secure random cannot be created */ - public static NKey createOperator(SecureRandom random) + public static NKey createOperator(@Nullable SecureRandom random) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { return createPair(NKeyType.OPERATOR, random); } @@ -229,7 +269,7 @@ public static NKey createOperator(SecureRandom random) * @throws NoSuchProviderException if the default secure random cannot be created * @throws NoSuchAlgorithmException if the default secure random cannot be created */ - public static NKey createServer(SecureRandom random) + public static NKey createServer(@Nullable SecureRandom random) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { return createPair(NKeyType.SERVER, random); } @@ -244,7 +284,7 @@ public static NKey createServer(SecureRandom random) * @throws NoSuchProviderException if the default secure random cannot be created * @throws NoSuchAlgorithmException if the default secure random cannot be created */ - public static NKey createUser(SecureRandom random) + public static NKey createUser(@Nullable SecureRandom random) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { return createPair(NKeyType.USER, random); } @@ -263,7 +303,8 @@ public static NKey fromPublicKey(char[] publicKey) { } NKeyType type = NKeyType.fromPrefix(prefix); - return new NKey(type, publicKey, null); + Provider securityPRovider = getSecurityProvider(); + return new NKey(type, publicKey, null, securityPRovider); } /** @@ -275,7 +316,8 @@ public static NKey fromSeed(char[] seed) { NKeyDecodedSeed decoded = decodeSeed(seed); // Should throw on bad seed if (decoded.bytes.length == ED25519_PRIVATE_KEYSIZE) { - return new NKey(NKeyType.fromPrefix(decoded.prefix), null, seed); + Provider securityProvider = getSecurityProvider(); + return new NKey(NKeyType.fromPrefix(decoded.prefix), null, seed, securityProvider); } else { try { return createPair(NKeyType.fromPrefix(decoded.prefix), decoded.bytes); @@ -328,19 +370,25 @@ public static boolean isValidPublicUserKey(char[] src) { /** * The seed or private key per the Ed25519 spec, encoded with encodeSeed. */ - private final char[] privateKeyAsSeed; + private final char @Nullable [] privateKeyAsSeed; /** * The public key, maybe null. Used for public only NKeys. */ - private final char[] publicKey; + private final char @Nullable [] publicKey; + + /** + * The Java Security API provider. + */ + private final @Nullable Provider securityProvider; private final NKeyType type; - private NKey(NKeyType t, char[] publicKey, char[] privateKey) { + private NKey(NKeyType t, char @Nullable [] publicKey, char @Nullable [] privateKey, @Nullable Provider securityProvider) { this.type = t; this.privateKeyAsSeed = privateKey; this.publicKey = publicKey; + this.securityProvider = securityProvider; } /** @@ -391,7 +439,8 @@ public char[] getPublicKey() throws GeneralSecurityException, IOException { if (publicKey != null) { return publicKey; } - return encode(this.type, getKeyPair().getPublic().getEncoded()); + byte[] pubBytes = KeyCodec.publicKeyToPubBytes(getKeyPair().getPublic()); + return encode(this.type, pubBytes); } /** @@ -428,10 +477,11 @@ public KeyPair getKeyPair() throws GeneralSecurityException, IOException { System.arraycopy(decoded.bytes, 0, seedBytes, 0, seedBytes.length); System.arraycopy(decoded.bytes, seedBytes.length, pubBytes, 0, pubBytes.length); - Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(seedBytes); - Ed25519PublicKeyParameters publicKey = new Ed25519PublicKeyParameters(pubBytes); + KeyFactory keyFactory = getKeyFactoryInstance(securityProvider); + PrivateKey privateKey = keyFactory.generatePrivate(KeyCodec.seedBytesToKeySpec(seedBytes)); + PublicKey publicKey = keyFactory.generatePublic(KeyCodec.pubBytesToKeySpec(pubBytes)); - return new KeyPair(new PublicKeyWrapper(publicKey), new PrivateKeyWrapper(privateKey)); + return new KeyPair(publicKey, privateKey); } /** @@ -442,7 +492,7 @@ public NKeyType getType() { } /** - * Sign aribitrary binary input. + * Sign arbitrary binary input. * * @param input the bytes to sign * @return the signature for the input from the NKey @@ -451,11 +501,10 @@ public NKeyType getType() { * @throws IOException if there is a problem reading the data */ public byte[] sign(byte[] input) throws GeneralSecurityException, IOException { - Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(getKeyPair().getPrivate().getEncoded()); - Ed25519Signer signer = new Ed25519Signer(); - signer.init(true, privateKey); - signer.update(input, 0, input.length); - return signer.generateSignature(); + Signature signature = getSignatureInstance(securityProvider); + signature.initSign(getKeyPair().getPrivate()); + signature.update(input); + return signature.sign(); } /** @@ -469,20 +518,19 @@ public byte[] sign(byte[] input) throws GeneralSecurityException, IOException { * @throws IOException if there is a problem reading the data */ public boolean verify(byte[] input, byte[] signature) throws GeneralSecurityException, IOException { - Ed25519PublicKeyParameters publicKey; + PublicKey publicKey; if (privateKeyAsSeed != null) { - publicKey = new Ed25519PublicKeyParameters(getKeyPair().getPublic().getEncoded()); + publicKey = getKeyPair().getPublic(); } else { char[] encodedPublicKey = getPublicKey(); byte[] decodedPublicKey = decode(this.type, encodedPublicKey); - //noinspection DataFlowIssue // decode will throw instead of return null - publicKey = new Ed25519PublicKeyParameters(decodedPublicKey); + KeyFactory keyFactory = getKeyFactoryInstance(securityProvider); + publicKey = keyFactory.generatePublic(KeyCodec.pubBytesToKeySpec(decodedPublicKey)); } - - Ed25519Signer signer = new Ed25519Signer(); - signer.init(false, publicKey); - signer.update(input, 0, input.length); - return signer.verifySignature(signature); + Signature signer = getSignatureInstance(securityProvider); + signer.initVerify(publicKey); + signer.update(input); + return signer.verify(signature); } @Override diff --git a/src/main/java/io/nats/nkey/NKeyConstants.java b/src/main/java/io/nats/nkey/NKeyConstants.java index c103e4d..b00498d 100644 --- a/src/main/java/io/nats/nkey/NKeyConstants.java +++ b/src/main/java/io/nats/nkey/NKeyConstants.java @@ -1,4 +1,4 @@ -// Copyright 2020-2024 The NATS Authors +// Copyright 2020-2025 The NATS Authors // 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: @@ -14,6 +14,9 @@ package io.nats.nkey; public interface NKeyConstants { + String CRYPTO_ALGORITHM = "Ed25519"; + String SECURITY_PROVIDER_PROPERTY = "io.nats.nkey.security.provider"; + // PrefixByteSeed is the prefix byte used for encoded NATS Seeds int PREFIX_BYTE_SEED = 18 << 3; // Base32-encodes to 'S...' diff --git a/src/main/java/io/nats/nkey/PrivateKeyWrapper.java b/src/main/java/io/nats/nkey/PrivateKeyWrapper.java deleted file mode 100644 index dc0f117..0000000 --- a/src/main/java/io/nats/nkey/PrivateKeyWrapper.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 The NATS Authors -// 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 io.nats.nkey; - -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; - -import java.security.PrivateKey; - -class PrivateKeyWrapper extends KeyWrapper implements PrivateKey { - - final Ed25519PrivateKeyParameters privateKey; - - public PrivateKeyWrapper(Ed25519PrivateKeyParameters privateKey) { - this.privateKey = privateKey; - } - - @Override - public byte[] getEncoded() { - return privateKey.getEncoded(); - } -} diff --git a/src/main/java/io/nats/nkey/PublicKeyWrapper.java b/src/main/java/io/nats/nkey/PublicKeyWrapper.java deleted file mode 100644 index a5d2871..0000000 --- a/src/main/java/io/nats/nkey/PublicKeyWrapper.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 The NATS Authors -// 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 io.nats.nkey; - -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; - -import java.security.PublicKey; - -class PublicKeyWrapper extends KeyWrapper implements PublicKey { - - final Ed25519PublicKeyParameters publicKey; - - public PublicKeyWrapper(Ed25519PublicKeyParameters publicKey) { - this.publicKey = publicKey; - } - - @Override - public byte[] getEncoded() { - return publicKey.getEncoded(); - } -} diff --git a/src/test/java/io/nats/nkey/NKeyTests.java b/src/test/java/io/nats/nkey/NKeyTests.java index 4266eea..456ab0e 100644 --- a/src/test/java/io/nats/nkey/NKeyTests.java +++ b/src/test/java/io/nats/nkey/NKeyTests.java @@ -1,4 +1,4 @@ -// Copyright 2020-2024 The NATS Authors +// Copyright 2020-2025 The NATS Authors // 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: @@ -14,12 +14,14 @@ package io.nats.nkey; import io.ResourceUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.SecureRandom; +import java.security.Signature; import java.util.Arrays; import java.util.Base64; import java.util.List; @@ -555,6 +557,17 @@ public void testInterop() throws Exception { NKeyDecodedSeed decoded = NKey.decodeSeed(seed); char[] encodedSeed = NKey.encodeSeed(NKeyType.fromPrefix(decoded.prefix), decoded.bytes); assertArrayEquals(encodedSeed, seed); + + Signature fromSeedJcaSignature = Signature.getInstance("Ed25519", new BouncyCastleProvider()); + fromSeedJcaSignature.initSign(fromSeed.getKeyPair().getPrivate()); + fromSeedJcaSignature.update(data); + byte[] fromSeedJcaSignatureBytes = fromSeedJcaSignature.sign(); + assertArrayEquals(fromSeedJcaSignatureBytes, sig); + + Signature fromPublicKeyJcaSignature = Signature.getInstance("Ed25519", new BouncyCastleProvider()); + fromPublicKeyJcaSignature.initVerify(fromSeed.getKeyPair().getPublic()); + fromPublicKeyJcaSignature.update(data); + assertTrue(fromPublicKeyJcaSignature.verify(sig)); } @Test