Skip to content

Commit 75a9183

Browse files
committed
FIPS Implementation
1 parent 7014402 commit 75a9183

File tree

11 files changed

+1728
-4
lines changed

11 files changed

+1728
-4
lines changed

core/src/test/java/io/nats/nkey/NKeyProviderTests.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.Arrays;
2323
import java.util.Base64;
24+
import java.util.List;
2425

2526
import static io.nats.nkey.NKeyConstants.NKEY_PROVIDER_CLASS_SYSTEM_PROPERTY;
2627
import static io.nats.nkey.NKeyProvider.getProvider;
@@ -464,4 +465,74 @@ private static void _testFromPublicKey(String userEncodedSeed, String userEncode
464465
assertArrayEquals(userEncodedPubKey.toCharArray(), fromSeed.getPublicKey());
465466
assertArrayEquals(userEncodedPubKey.toCharArray(), fromKey.getPublicKey());
466467
}
468+
469+
static byte[] TO_SIGN = "Synadia".getBytes(StandardCharsets.UTF_8);
470+
471+
@Test
472+
public void testFromText() {
473+
List<String> inputs = ResourceUtils.resourceAsLines("test-nkeys.txt");
474+
for (int i = 0; i < inputs.size(); ) {
475+
String name = inputs.get(i++);
476+
int prefix = Integer.parseInt(inputs.get(i++));
477+
char[] seed = inputs.get(i++).toCharArray();
478+
char[] publicKey = inputs.get(i++).toCharArray();
479+
char[] privateKey = inputs.get(i++).toCharArray();
480+
byte[] decoded = toBytes(inputs.get(i++));
481+
byte[] signed = toBytes(inputs.get(i++));
482+
483+
NKeyType type = NKeyType.fromPrefix(prefix);
484+
assertNotNull(type);
485+
assertEquals(name, type.name());
486+
487+
NKey fromSeed = PROVIDER.fromSeed(seed);
488+
NKey fromPublicKey = PROVIDER.fromPublicKey(publicKey);
489+
490+
assertArrayEquals(seed, fromSeed.getSeed());
491+
assertArrayEquals(publicKey, fromSeed.getPublicKey());
492+
assertArrayEquals(privateKey, fromSeed.getPrivateKey());
493+
assertArrayEquals(publicKey, fromPublicKey.getPublicKey());
494+
assertEquals(prefix, fromSeed.getDecodedSeed().prefix);
495+
assertArrayEquals(decoded, fromSeed.getDecodedSeed().bytes);
496+
assertArrayEquals(signed, fromSeed.sign(TO_SIGN));
497+
}
498+
}
499+
500+
@Test
501+
public void testGenerateTestNkeysText() {
502+
for (int x = 0; x < 10; x++) {
503+
generateTestNkeysText(PROVIDER.createUser());
504+
generateTestNkeysText(PROVIDER.createAccount());
505+
generateTestNkeysText(PROVIDER.createOperator());
506+
generateTestNkeysText(PROVIDER.createServer());
507+
generateTestNkeysText(PROVIDER.createCluster());
508+
}
509+
}
510+
511+
private static void generateTestNkeysText(NKey theKey) {
512+
char[] seed = theKey.getSeed();
513+
char[] publicKey = theKey.getPublicKey();
514+
char[] privateKey = theKey.getPrivateKey();
515+
System.out.println(theKey.getType());
516+
System.out.println(theKey.getType().prefix);
517+
System.out.println(new String(seed));
518+
System.out.println(new String(publicKey));
519+
System.out.println(new String(privateKey));
520+
byte[] bytes = theKey.getDecodedSeed().bytes;
521+
System.out.println(toString(bytes));
522+
bytes = theKey.sign(TO_SIGN);
523+
System.out.println(toString(bytes));
524+
}
525+
526+
private static String toString(byte[] bytes) {
527+
return Arrays.toString(bytes).replace("[", "").replace("]", "").replace(" ", "");
528+
}
529+
530+
private byte[] toBytes(String s) {
531+
String[] split = s.split(",");
532+
byte[] decoded = new byte[split.length];
533+
for (int i = 0; i < split.length; i++) {
534+
decoded[i] = (byte) Integer.parseInt(split[i]);
535+
}
536+
return decoded;
537+
}
467538
}

core/src/test/resources/test-nkeys.txt

Lines changed: 350 additions & 0 deletions
Large diffs are not rendered by default.

fips/src/main/java/io/nats/nkey/FipsNKeyProvider.java

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
package io.nats.nkey;
22

3+
import org.bouncycastle.crypto.UpdateOutputStream;
4+
import org.bouncycastle.crypto.asymmetric.AsymmetricEdDSAPrivateKey;
5+
import org.bouncycastle.crypto.asymmetric.AsymmetricEdDSAPublicKey;
6+
import org.bouncycastle.crypto.fips.FipsEdEC;
7+
import org.bouncycastle.crypto.fips.FipsOutputSigner;
8+
import org.bouncycastle.crypto.fips.FipsOutputVerifier;
39
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
410
import org.jspecify.annotations.NullMarked;
511

12+
import java.io.IOException;
613
import java.security.*;
714

15+
import static io.nats.nkey.NKeyConstants.ED25519_PUBLIC_KEYSIZE;
16+
import static io.nats.nkey.NKeyConstants.ED25519_SEED_SIZE;
17+
import static io.nats.nkey.NKeyProviderUtils.encodeSeed;
18+
import static io.nats.nkey.NKeyProviderUtils.nkeyDecode;
19+
820
@NullMarked
921
public class FipsNKeyProvider extends NKeyProvider {
1022
static {
@@ -26,7 +38,14 @@ public FipsNKeyProvider() {
2638
*/
2739
@Override
2840
public NKey createNKey(NKeyType type, byte[] seed) {
29-
throw new UnsupportedOperationException("createPair not supported yet.");
41+
byte[] pubBytes = FipsEdEC.computePublicData(FipsEdEC.Ed25519.getAlgorithm(), seed);
42+
43+
byte[] bytes = new byte[pubBytes.length + seed.length];
44+
System.arraycopy(seed, 0, bytes, 0, seed.length);
45+
System.arraycopy(pubBytes, 0, bytes, seed.length, pubBytes.length);
46+
47+
char[] encoded = encodeSeed(type, bytes);
48+
return new NKey(this, type, null, encoded);
3049
}
3150

3251
/**
@@ -35,22 +54,69 @@ public NKey createNKey(NKeyType type, byte[] seed) {
3554
@Override
3655
public KeyPair getKeyPair(NKey nkey) {
3756
nkey.ensurePair();
38-
throw new UnsupportedOperationException("getKeyPair not supported yet.");
57+
NKeyDecodedSeed decoded = nkey.getDecodedSeed();
58+
byte[] seedBytes = new byte[ED25519_SEED_SIZE];
59+
byte[] pubBytes = new byte[ED25519_PUBLIC_KEYSIZE];
60+
61+
System.arraycopy(decoded.bytes, 0, seedBytes, 0, seedBytes.length);
62+
System.arraycopy(decoded.bytes, seedBytes.length, pubBytes, 0, pubBytes.length);
63+
64+
AsymmetricEdDSAPrivateKey privateKey = new AsymmetricEdDSAPrivateKey(FipsEdEC.Ed25519.getAlgorithm(), seedBytes, pubBytes);
65+
AsymmetricEdDSAPublicKey publicKey = new AsymmetricEdDSAPublicKey(FipsEdEC.Ed25519.getAlgorithm(), pubBytes);
66+
67+
return new KeyPair(new PublicKeyWrapper(publicKey), new PrivateKeyWrapper(privateKey));
3968
}
4069

4170
/**
4271
* {@inheritDoc}
4372
*/
4473
@Override
4574
public byte[] sign(NKey nkey, byte[] input) {
46-
throw new UnsupportedOperationException("sign not supported yet.");
75+
byte[] seedBytes = nkey.getKeyPair().getPrivate().getEncoded();
76+
byte[] pubBytes = nkey.getKeyPair().getPublic().getEncoded();
77+
AsymmetricEdDSAPrivateKey privateKey = new AsymmetricEdDSAPrivateKey(FipsEdEC.Ed25519.getAlgorithm(), seedBytes, pubBytes);
78+
79+
FipsEdEC.EdDSAOperatorFactory factory = new FipsEdEC.EdDSAOperatorFactory();
80+
FipsOutputSigner<FipsEdEC.Parameters> signer = factory.createSigner(privateKey, FipsEdEC.EdDSA);
81+
82+
try {
83+
UpdateOutputStream stream = signer.getSigningStream();
84+
stream.update(input, 0, input.length);
85+
stream.close();
86+
return signer.getSignature();
87+
}
88+
catch (IOException e) {
89+
throw new RuntimeException(e);
90+
}
4791
}
4892

4993
/**
5094
* {@inheritDoc}
5195
*/
5296
@Override
5397
public boolean verify(NKey nkey, byte[] input, byte[] signature) {
54-
throw new UnsupportedOperationException("verify not supported yet.");
98+
AsymmetricEdDSAPublicKey publicKey;
99+
if (nkey.isPair()) {
100+
byte[] pubBytes = nkey.getKeyPair().getPublic().getEncoded();
101+
publicKey = new AsymmetricEdDSAPublicKey(FipsEdEC.Ed25519.getAlgorithm(), pubBytes);
102+
}
103+
else {
104+
char[] encodedPublicKey = nkey.getPublicKey();
105+
byte[] decodedPublicKey = nkeyDecode(nkey.getType(), encodedPublicKey);
106+
publicKey = new AsymmetricEdDSAPublicKey(FipsEdEC.Ed25519.getAlgorithm(), decodedPublicKey);
107+
}
108+
109+
FipsEdEC.EdDSAOperatorFactory factory = new FipsEdEC.EdDSAOperatorFactory();
110+
FipsOutputVerifier<FipsEdEC.Parameters> verifier = factory.createVerifier(publicKey, FipsEdEC.EdDSA);
111+
112+
try {
113+
UpdateOutputStream stream = verifier.getVerifyingStream();
114+
stream.update(input, 0, input.length);
115+
stream.close();
116+
return verifier.isVerified(signature);
117+
}
118+
catch (IOException e) {
119+
throw new RuntimeException(e);
120+
}
55121
}
56122
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2025-2026 The NATS Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at:
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package io.nats.nkey;
15+
16+
import org.bouncycastle.crypto.asymmetric.AsymmetricEdDSAPrivateKey;
17+
18+
import java.security.PrivateKey;
19+
20+
class PrivateKeyWrapper extends KeyWrapper implements PrivateKey {
21+
22+
final AsymmetricEdDSAPrivateKey privateKey;
23+
24+
public PrivateKeyWrapper(AsymmetricEdDSAPrivateKey privateKey) {
25+
this.privateKey = privateKey;
26+
}
27+
28+
@Override
29+
public byte[] getEncoded() {
30+
return privateKey.getSecret();
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2025-2026 The NATS Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at:
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package io.nats.nkey;
15+
16+
import org.bouncycastle.crypto.asymmetric.AsymmetricEdDSAPublicKey;
17+
18+
import java.security.PublicKey;
19+
20+
class PublicKeyWrapper extends KeyWrapper implements PublicKey {
21+
22+
final AsymmetricEdDSAPublicKey publicKey;
23+
24+
public PublicKeyWrapper(AsymmetricEdDSAPublicKey publicKey) {
25+
this.publicKey = publicKey;
26+
}
27+
28+
@Override
29+
public byte[] getEncoded() {
30+
return publicKey.getPublicData();
31+
}
32+
}

fips/src/test/java/io/nats/nkey/FipsProviderTests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.Arrays;
2323
import java.util.Base64;
24+
import java.util.List;
2425

2526
import static io.nats.nkey.NKeyConstants.NKEY_PROVIDER_CLASS_SYSTEM_PROPERTY;
2627
import static io.nats.nkey.NKeyProvider.getProvider;
@@ -463,4 +464,44 @@ private static void _testFromPublicKey(String userEncodedSeed, String userEncode
463464
assertArrayEquals(userEncodedPubKey.toCharArray(), fromSeed.getPublicKey());
464465
assertArrayEquals(userEncodedPubKey.toCharArray(), fromKey.getPublicKey());
465466
}
467+
468+
static byte[] TO_SIGN = "Synadia".getBytes(StandardCharsets.UTF_8);
469+
470+
@Test
471+
public void testFromText() {
472+
List<String> inputs = ResourceUtils.resourceAsLines("test-nkeys.txt");
473+
for (int i = 0; i < inputs.size(); ) {
474+
String name = inputs.get(i++);
475+
int prefix = Integer.parseInt(inputs.get(i++));
476+
char[] seed = inputs.get(i++).toCharArray();
477+
char[] publicKey = inputs.get(i++).toCharArray();
478+
char[] privateKey = inputs.get(i++).toCharArray();
479+
byte[] decoded = toBytes(inputs.get(i++));
480+
byte[] signed = toBytes(inputs.get(i++));
481+
482+
NKeyType type = NKeyType.fromPrefix(prefix);
483+
assertNotNull(type);
484+
assertEquals(name, type.name());
485+
486+
NKey fromSeed = PROVIDER.fromSeed(seed);
487+
NKey fromPublicKey = PROVIDER.fromPublicKey(publicKey);
488+
489+
assertArrayEquals(seed, fromSeed.getSeed());
490+
assertArrayEquals(publicKey, fromSeed.getPublicKey());
491+
assertArrayEquals(privateKey, fromSeed.getPrivateKey());
492+
assertArrayEquals(publicKey, fromPublicKey.getPublicKey());
493+
assertEquals(prefix, fromSeed.getDecodedSeed().prefix);
494+
assertArrayEquals(decoded, fromSeed.getDecodedSeed().bytes);
495+
assertArrayEquals(signed, fromSeed.sign(TO_SIGN));
496+
}
497+
}
498+
499+
private byte[] toBytes(String s) {
500+
String[] split = s.split(",");
501+
byte[] decoded = new byte[split.length];
502+
for (int i = 0; i < split.length; i++) {
503+
decoded[i] = (byte) Integer.parseInt(split[i]);
504+
}
505+
return decoded;
506+
}
466507
}

0 commit comments

Comments
 (0)