Skip to content

Commit 6a3e356

Browse files
authored
Merge pull request #26 from nats-io/fips-impl
FIPS Implementation
2 parents e7a1274 + 484853c commit 6a3e356

File tree

8 files changed

+646
-4
lines changed

8 files changed

+646
-4
lines changed

.github/workflows/fips-main.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: FIPS Main Snapshot
2+
permissions:
3+
contents: read
4+
on:
5+
push:
6+
branches:
7+
- main
8+
paths:
9+
- 'fips/**'
10+
11+
jobs:
12+
build:
13+
strategy:
14+
matrix:
15+
tc: [ 17, 21, 25 ]
16+
runs-on: ubuntu-latest
17+
defaults:
18+
run:
19+
working-directory: ./fips
20+
env:
21+
BUILD_EVENT: ${{ github.event_name }}
22+
TARGET_COMPATIBILITY: ${{ matrix.tc }}
23+
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
24+
OSSRH_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
25+
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
26+
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
27+
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
28+
steps:
29+
- name: Set up JDK
30+
uses: actions/setup-java@v5
31+
with:
32+
java-version: 25
33+
distribution: 'temurin'
34+
- name: Setup Gradle
35+
uses: gradle/actions/setup-gradle@v5
36+
with:
37+
gradle-version: current
38+
- name: Check out code
39+
uses: actions/checkout@v4
40+
- name: Build and Test
41+
run: chmod +x gradlew && ./gradlew clean test jacocoTestReport
42+
- name: Verify Javadoc
43+
run: ./gradlew javadoc
44+
- name: Publish Snapshot
45+
run: ./gradlew -i publishToSonatype

.github/workflows/fips-pr.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Fips Pull Request
2+
permissions:
3+
contents: read
4+
on:
5+
pull_request:
6+
types: [opened, synchronize, reopened]
7+
paths:
8+
- 'fips/**'
9+
10+
jobs:
11+
build:
12+
strategy:
13+
matrix:
14+
tc: [ 17, 21, 25 ]
15+
runs-on: ubuntu-latest
16+
defaults:
17+
run:
18+
working-directory: ./fips
19+
env:
20+
BUILD_EVENT: ${{ github.event_name }}
21+
TARGET_COMPATIBILITY: ${{ matrix.tc }}
22+
steps:
23+
- name: Set up JDK
24+
uses: actions/setup-java@v5
25+
with:
26+
java-version: 25
27+
distribution: 'temurin'
28+
- name: Setup Gradle
29+
uses: gradle/actions/setup-gradle@v5
30+
with:
31+
gradle-version: current
32+
- name: Check out code
33+
uses: actions/checkout@v4
34+
- name: Build and Test
35+
run: chmod +x gradlew && ./gradlew clean test jacocoTestReport
36+
- name: Verify Javadoc
37+
run: ./gradlew javadoc

.github/workflows/fips-release.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Fips Publish Release
2+
permissions:
3+
contents: read
4+
on:
5+
push:
6+
tags: [ 'fips/*' ]
7+
8+
jobs:
9+
build:
10+
strategy:
11+
matrix:
12+
tc: [ 17, 21, 25 ]
13+
runs-on: ubuntu-latest
14+
defaults:
15+
run:
16+
working-directory: ./fips
17+
env:
18+
BUILD_EVENT: "release"
19+
TARGET_COMPATIBILITY: ${{ matrix.tc }}
20+
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
21+
OSSRH_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
22+
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
23+
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
24+
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
25+
steps:
26+
- name: Set up JDK
27+
uses: actions/setup-java@v5
28+
with:
29+
java-version: 25
30+
distribution: 'temurin'
31+
- name: Setup Gradle
32+
uses: gradle/actions/setup-gradle@v5
33+
with:
34+
gradle-version: current
35+
- name: Check out code
36+
uses: actions/checkout@v4
37+
- name: Build, Sign and Publish Release
38+
run: chmod +x gradlew && ./gradlew clean compileJava publishToSonatype closeAndReleaseSonatypeStagingRepository

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

Lines changed: 71 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,70 @@ 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+
KeyPair keyPair = nkey.getKeyPair();
76+
byte[] seedBytes = keyPair.getPrivate().getEncoded();
77+
byte[] pubBytes = keyPair.getPublic().getEncoded();
78+
AsymmetricEdDSAPrivateKey privateKey = new AsymmetricEdDSAPrivateKey(FipsEdEC.Ed25519.getAlgorithm(), seedBytes, pubBytes);
79+
80+
FipsEdEC.EdDSAOperatorFactory factory = new FipsEdEC.EdDSAOperatorFactory();
81+
FipsOutputSigner<FipsEdEC.Parameters> signer = factory.createSigner(privateKey, FipsEdEC.EdDSA);
82+
83+
try {
84+
UpdateOutputStream stream = signer.getSigningStream();
85+
stream.update(input, 0, input.length);
86+
stream.finished();
87+
return signer.getSignature();
88+
}
89+
catch (IOException e) {
90+
throw new RuntimeException(e);
91+
}
4792
}
4893

4994
/**
5095
* {@inheritDoc}
5196
*/
5297
@Override
5398
public boolean verify(NKey nkey, byte[] input, byte[] signature) {
54-
throw new UnsupportedOperationException("verify not supported yet.");
99+
AsymmetricEdDSAPublicKey publicKey;
100+
if (nkey.isPair()) {
101+
byte[] pubBytes = nkey.getKeyPair().getPublic().getEncoded();
102+
publicKey = new AsymmetricEdDSAPublicKey(FipsEdEC.Ed25519.getAlgorithm(), pubBytes);
103+
}
104+
else {
105+
char[] encodedPublicKey = nkey.getPublicKey();
106+
byte[] decodedPublicKey = nkeyDecode(nkey.getType(), encodedPublicKey);
107+
publicKey = new AsymmetricEdDSAPublicKey(FipsEdEC.Ed25519.getAlgorithm(), decodedPublicKey);
108+
}
109+
110+
FipsEdEC.EdDSAOperatorFactory factory = new FipsEdEC.EdDSAOperatorFactory();
111+
FipsOutputVerifier<FipsEdEC.Parameters> verifier = factory.createVerifier(publicKey, FipsEdEC.EdDSA);
112+
113+
try {
114+
UpdateOutputStream stream = verifier.getVerifyingStream();
115+
stream.update(input, 0, input.length);
116+
stream.close();
117+
return verifier.isVerified(signature);
118+
}
119+
catch (IOException e) {
120+
throw new RuntimeException(e);
121+
}
55122
}
56123
}
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)