From ae54ea686113a2bc3b59463ffd5f7f1e89257133 Mon Sep 17 00:00:00 2001 From: Adam Preuss Date: Wed, 21 Jan 2026 15:12:26 -0700 Subject: [PATCH] Add support for deterministic ECDSA Use BeforeEach for a cleaner test setup. --- .../biscuit/crypto/SECP256R1KeyPair.java | 21 +++++- .../org/eclipse/biscuit/token/Biscuit.java | 8 ++- .../biscuit/token/UnverifiedBiscuit.java | 8 ++- .../eclipse/biscuit/crypto/SignatureTest.java | 24 ++++--- .../eclipse/biscuit/token/AuthorizerTest.java | 14 +++- .../eclipse/biscuit/token/BiscuitTest.java | 37 ++++------ .../eclipse/biscuit/token/ThirdPartyTest.java | 72 ++++++++++++++++--- .../biscuit/token/UnverifiedBiscuitTest.java | 11 ++- 8 files changed, 142 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/eclipse/biscuit/crypto/SECP256R1KeyPair.java b/src/main/java/org/eclipse/biscuit/crypto/SECP256R1KeyPair.java index 805530eb..e891c9e2 100644 --- a/src/main/java/org/eclipse/biscuit/crypto/SECP256R1KeyPair.java +++ b/src/main/java/org/eclipse/biscuit/crypto/SECP256R1KeyPair.java @@ -9,6 +9,7 @@ import java.security.SecureRandom; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; import org.bouncycastle.crypto.signers.StandardDSAEncoding; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; @@ -67,14 +68,32 @@ final class SECP256R1KeyPair extends KeyPair { this.publicKey = publicKey; } + /// By default sign message digests with a deterministic k + /// computed using the algorithm described in [RFC6979 § 3.2]. + /// + /// [RFC6979 § 3.2]: https://tools.ietf.org/html/rfc6979#section-3 + /// + /// Although deterministic ECDSA signing is typically slower than + /// signing with an RNG, it prevents accidental nonce-reuse due to + /// a weak RNG. @Override public byte[] sign(byte[] data) { + return sign(data, true); + } + + public byte[] sign(byte[] data, boolean deterministicNonce) { var digest = new SHA256Digest(); digest.update(data, 0, data.length); var hash = new byte[digest.getDigestSize()]; digest.doFinal(hash, 0); - var signer = new ECDSASigner(); + ECDSASigner signer; + if (deterministicNonce) { + signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + } else { + signer = new ECDSASigner(); + } + signer.init(true, privateKey.engineGetKeyParameters()); var sig = signer.generateSignature(hash); diff --git a/src/main/java/org/eclipse/biscuit/token/Biscuit.java b/src/main/java/org/eclipse/biscuit/token/Biscuit.java index adcb8018..df3173c4 100644 --- a/src/main/java/org/eclipse/biscuit/token/Biscuit.java +++ b/src/main/java/org/eclipse/biscuit/token/Biscuit.java @@ -356,7 +356,13 @@ public Biscuit attenuate(final SecureRandom rng, final KeyPair keypair, Block bl /** Generates a third party block request from a token */ public Biscuit appendThirdPartyBlock(PublicKey externalKey, ThirdPartyBlockContents blockResponse) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { - UnverifiedBiscuit b = super.appendThirdPartyBlock(externalKey, blockResponse); + return appendThirdPartyBlock(externalKey, blockResponse, new SecureRandom()); + } + + public Biscuit appendThirdPartyBlock( + PublicKey externalKey, ThirdPartyBlockContents blockResponse, SecureRandom rng) + throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { + UnverifiedBiscuit b = super.appendThirdPartyBlock(externalKey, blockResponse, rng); // no need to verify again, we are already working from a verified token return Biscuit.fromSerializedBiscuit(b.serializedBiscuit, b.symbolTable); diff --git a/src/main/java/org/eclipse/biscuit/token/UnverifiedBiscuit.java b/src/main/java/org/eclipse/biscuit/token/UnverifiedBiscuit.java index 2c84c982..006d73ac 100644 --- a/src/main/java/org/eclipse/biscuit/token/UnverifiedBiscuit.java +++ b/src/main/java/org/eclipse/biscuit/token/UnverifiedBiscuit.java @@ -279,6 +279,12 @@ public ThirdPartyBlockRequest thirdPartyRequest() { public UnverifiedBiscuit appendThirdPartyBlock( PublicKey externalKey, ThirdPartyBlockContents blockResponse) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { + return appendThirdPartyBlock(externalKey, blockResponse, new SecureRandom()); + } + + public UnverifiedBiscuit appendThirdPartyBlock( + PublicKey externalKey, ThirdPartyBlockContents blockResponse, SecureRandom rng) + throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { SignedBlock previousBlock; if (this.serializedBiscuit.getBlocks().isEmpty()) { previousBlock = this.serializedBiscuit.getAuthority(); @@ -286,7 +292,7 @@ public UnverifiedBiscuit appendThirdPartyBlock( previousBlock = this.serializedBiscuit.getBlocks().get(this.serializedBiscuit.getBlocks().size() - 1); } - KeyPair nextKeyPair = KeyPair.generate(previousBlock.getKey().getAlgorithm()); + KeyPair nextKeyPair = KeyPair.generate(previousBlock.getKey().getAlgorithm(), rng); byte[] payload = BlockSignatureBuffer.generateExternalBlockSignaturePayloadV1( blockResponse.getPayload(), diff --git a/src/test/java/org/eclipse/biscuit/crypto/SignatureTest.java b/src/test/java/org/eclipse/biscuit/crypto/SignatureTest.java index c6f6df99..b818b815 100644 --- a/src/test/java/org/eclipse/biscuit/crypto/SignatureTest.java +++ b/src/test/java/org/eclipse/biscuit/crypto/SignatureTest.java @@ -18,14 +18,24 @@ import org.eclipse.biscuit.error.Error; import org.eclipse.biscuit.error.Result; import org.eclipse.biscuit.token.Biscuit; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * @serial exclude */ public class SignatureTest { + private SecureRandom rng; + + @BeforeEach + public void setUp() throws NoSuchAlgorithmException { + byte[] seed = {0, 0, 0, 0}; + rng = SecureRandom.getInstance("SHA1PRNG"); + rng.setSeed(seed); + } + @Test - public void testSerialize() throws Error.FormatError { + public void testSerialize() throws Error.FormatError, NoSuchAlgorithmException { prTestSerialize(Schema.PublicKey.Algorithm.Ed25519, 32); prTestSerialize( // compressed - 0x02 or 0x03 prefix byte, 32 bytes for X coordinate @@ -86,11 +96,8 @@ void testInvalidEd25519PublicKey() { () -> PublicKey.load(Schema.PublicKey.Algorithm.Ed25519, "badkey".getBytes())); } - private static void prTestSerialize( - Schema.PublicKey.Algorithm algorithm, int expectedPublicKeyLength) throws Error.FormatError { - byte[] seed = {1, 2, 3, 4}; - SecureRandom rng = new SecureRandom(seed); - + private void prTestSerialize(Schema.PublicKey.Algorithm algorithm, int expectedPublicKeyLength) + throws Error.FormatError, NoSuchAlgorithmException { KeyPair keypair = KeyPair.generate(algorithm, rng); PublicKey pubkey = keypair.getPublicKey(); @@ -112,11 +119,8 @@ private static void prTestSerialize( assertEquals(pubkey.toHex(), deserializedPublicKey.toHex()); } - private static void prTestThreeMessages(Schema.PublicKey.Algorithm algorithm) + private void prTestThreeMessages(Schema.PublicKey.Algorithm algorithm) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - String message1 = "hello"; KeyPair root = KeyPair.generate(algorithm, rng); KeyPair keypair2 = KeyPair.generate(algorithm, rng); diff --git a/src/test/java/org/eclipse/biscuit/token/AuthorizerTest.java b/src/test/java/org/eclipse/biscuit/token/AuthorizerTest.java index a5e4e1ff..8ea9d9b2 100644 --- a/src/test/java/org/eclipse/biscuit/token/AuthorizerTest.java +++ b/src/test/java/org/eclipse/biscuit/token/AuthorizerTest.java @@ -22,10 +22,19 @@ import org.eclipse.biscuit.error.Error.Parser; import org.eclipse.biscuit.token.builder.Expression; import org.eclipse.biscuit.token.builder.Term; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class AuthorizerTest { final RunLimits runLimits = new RunLimits(500, 100, Duration.ofMillis(500)); + private SecureRandom rng; + + @BeforeEach + public void setUp() throws Exception { + byte[] seed = {0, 0, 0, 0}; + rng = SecureRandom.getInstance("SHA1PRNG"); + rng.setSeed(seed); + } @Test public void testAuthorizerPolicy() throws Parser { @@ -51,8 +60,7 @@ public void testAuthorizerPolicy() throws Parser { @Test public void testPuttingSomeFactsInBiscuitAndGettingThemBackOutAgain() throws Exception { - - KeyPair keypair = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, new SecureRandom()); + KeyPair keypair = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); Biscuit token = Biscuit.builder(keypair) @@ -84,7 +92,7 @@ public void testPuttingSomeFactsInBiscuitAndGettingThemBackOutAgain() throws Exc @Test public void testDatalogAuthorizer() throws Exception { - KeyPair keypair = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, new SecureRandom()); + KeyPair keypair = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); Biscuit token = Biscuit.builder(keypair) diff --git a/src/test/java/org/eclipse/biscuit/token/BiscuitTest.java b/src/test/java/org/eclipse/biscuit/token/BiscuitTest.java index 0bbe0e16..559ba191 100644 --- a/src/test/java/org/eclipse/biscuit/token/BiscuitTest.java +++ b/src/test/java/org/eclipse/biscuit/token/BiscuitTest.java @@ -37,15 +37,23 @@ import org.eclipse.biscuit.error.LogicError; import org.eclipse.biscuit.token.builder.Block; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BiscuitTest { + private SecureRandom rng; + + @BeforeEach + public void setUp() throws NoSuchAlgorithmException { + byte[] seed = {0, 0, 0, 0}; + rng = SecureRandom.getInstance("SHA1PRNG"); + rng.setSeed(seed); + } + @Test public void testBasic() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); System.out.println("preparing the authority block"); @@ -177,9 +185,6 @@ public void testBasic() @Test public void testFolders() throws NoSuchAlgorithmException, Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -288,10 +293,7 @@ public void testMultipleAttenuation() } @Test - public void testReset() throws Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - + public void testReset() throws Error, NoSuchAlgorithmException { System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -361,10 +363,7 @@ public void testReset() throws Error { } @Test - public void testEmptyAuthorizer() throws Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - + public void testEmptyAuthorizer() throws Error, NoSuchAlgorithmException { System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -405,9 +404,6 @@ public void testEmptyAuthorizer() throws Error { @Test public void testBasicWithNamespaces() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -535,9 +531,6 @@ public void testBasicWithNamespaces() @Test public void testBasicWithNamespacesWithAddAuthorityFact() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -664,9 +657,6 @@ public void testBasicWithNamespacesWithAddAuthorityFact() @Test public void testRootKeyId() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -739,9 +729,6 @@ public Optional getRootKey(Optional keyId) { @Test public void testCheckAll() throws Error, NoSuchAlgorithmException, SignatureException, InvalidKeyException { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - System.out.println("preparing the authority block"); KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); diff --git a/src/test/java/org/eclipse/biscuit/token/ThirdPartyTest.java b/src/test/java/org/eclipse/biscuit/token/ThirdPartyTest.java index a7cd3057..658c281b 100644 --- a/src/test/java/org/eclipse/biscuit/token/ThirdPartyTest.java +++ b/src/test/java/org/eclipse/biscuit/token/ThirdPartyTest.java @@ -23,9 +23,19 @@ import org.eclipse.biscuit.error.FailedCheck; import org.eclipse.biscuit.error.LogicError; import org.eclipse.biscuit.token.builder.Block; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class ThirdPartyTest { + private SecureRandom rng; + + @BeforeEach + public void setUp() throws NoSuchAlgorithmException { + byte[] seed = {0, 0, 0, 0}; + rng = SecureRandom.getInstance("SHA1PRNG"); + rng.setSeed(seed); + } + @Test public void testRoundTrip() throws NoSuchAlgorithmException, @@ -34,8 +44,6 @@ public void testRoundTrip() CloneNotSupportedException, Error, IOException { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); System.out.println("preparing the authority block"); @@ -105,11 +113,6 @@ public void testPublicKeyInterning() InvalidKeyException, CloneNotSupportedException, Error { - // this makes a deterministic RNG - SecureRandom rng = SecureRandom.getInstance("SHA1PRNG"); - byte[] seed = {0, 0, 0, 0}; - rng.setSeed(seed); - System.out.println("preparing the authority block"); final KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -200,9 +203,6 @@ public void testReusedSymbols() InvalidKeyException, CloneNotSupportedException, Error { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); - System.out.println("preparing the authority block"); final KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.Ed25519, rng); @@ -254,4 +254,56 @@ public void testReusedSymbols() e); } } + + private org.eclipse.biscuit.token.Biscuit generateDeterministicBiscuit(SecureRandom rng) + throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { + final KeyPair root = KeyPair.generate(Schema.PublicKey.Algorithm.SECP256R1, rng); + final KeyPair external = KeyPair.generate(Schema.PublicKey.Algorithm.SECP256R1, rng); + + Block authorityBuilder = new Block(); + Biscuit b1 = Biscuit.make(rng, root, authorityBuilder.build()); + + ThirdPartyBlockRequest request = b1.thirdPartyRequest(); + Block builder = new Block(); + ThirdPartyBlockContents blockResponse = request.createBlock(external, builder).getOk(); + Biscuit b2 = b1.appendThirdPartyBlock(external.getPublicKey(), blockResponse, rng); + + return b2; + } + + @Test + public void testDeterministicECDSA() + throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, Error { + // Generate the same set of root and 3rd-party keys and confirm that they result in the same + // ECDSA signatures. + + // Create fresh RNG with same seed for first biscuit + byte[] seed = {0, 0, 0, 0}; + SecureRandom rng1 = SecureRandom.getInstance("SHA1PRNG"); + rng1.setSeed(seed); + Biscuit b1 = generateDeterministicBiscuit(rng1); + + // Create fresh RNG with same seed for second biscuit to get identical signatures + SecureRandom rng2 = SecureRandom.getInstance("SHA1PRNG"); + rng2.setSeed(seed); + Biscuit b2 = generateDeterministicBiscuit(rng2); + + assert (Arrays.equals( + b1.serializedBiscuit.getAuthority().getSignature(), + b2.serializedBiscuit.getAuthority().getSignature())); + + assert (Arrays.equals( + b1.serializedBiscuit.getBlocks().get(0).getSignature(), + b2.serializedBiscuit.getBlocks().get(0).getSignature())); + + byte[] data1 = b1.serialize(); + byte[] data2 = b2.serialize(); + assert (Arrays.equals(data1, data2)); + assertEquals(b1.print(), b2.print()); + + // Make sure that the token is still valid + Authorizer authorizer = b1.authorizer(); + authorizer.addPolicy("allow if true"); + authorizer.authorize(new RunLimits(500, 100, Duration.ofMillis(500))); + } } diff --git a/src/test/java/org/eclipse/biscuit/token/UnverifiedBiscuitTest.java b/src/test/java/org/eclipse/biscuit/token/UnverifiedBiscuitTest.java index faf7a633..76b0770d 100644 --- a/src/test/java/org/eclipse/biscuit/token/UnverifiedBiscuitTest.java +++ b/src/test/java/org/eclipse/biscuit/token/UnverifiedBiscuitTest.java @@ -23,15 +23,22 @@ import org.eclipse.biscuit.error.LogicError; import org.eclipse.biscuit.token.builder.Block; import org.eclipse.biscuit.token.builder.Utils; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class UnverifiedBiscuitTest { + private SecureRandom rng; + + @BeforeEach + public void setUp() throws NoSuchAlgorithmException { + byte[] seed = {0, 0, 0, 0}; + rng = SecureRandom.getInstance("SHA1PRNG"); + rng.setSeed(seed); + } @Test public void testBasic() throws Error, NoSuchAlgorithmException, SignatureException, InvalidKeyException { - byte[] seed = {0, 0, 0, 0}; - SecureRandom rng = new SecureRandom(seed); System.out.println("preparing the authority block, block0");