Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 18 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ _Secret key cryptography is **not** supported. PRs to add secret key cryptograph
- Ed25519
- ECDSA
- secp256k1

[comment]: <> ( - secp256r1)
- secp256r1
- Falcon

## Hashing algorithms

Expand Down Expand Up @@ -65,7 +65,7 @@ Verify a signature using a public key or a KeyPair.

_A `sign` method which prepends the message to the signature, compatible with
[libsodium's combined mode](https://libsodium.gitbook.io/doc/public-key_cryptography/public-key_signatures#combined-mode),
is not yet supported._
is not (yet) supported._

## Example usages

Expand All @@ -82,7 +82,7 @@ Signature mySignature = secp256k1.signDetached(myMessage, myKeyPair);

assert sig.getBytes().length == 65; // including header recId byte

secp256k1.verify(myMessage, mySignature, myKeyPair) // True
secp256k1.verify(myMessage, mySignature, myKeyPair); // True
```

Create an `ECDSA` object, using `secp256k1` curve with default `Keccak-256` digest,
Expand All @@ -98,25 +98,9 @@ Signature mySignature = secp256k1.signDetached(myMessage, myKeyPair);

assert sig.getBytes().length == 64;

secp256k1.verify(myMessage, mySignature, myKeyPair) // True
secp256k1.verify(myMessage, mySignature, myKeyPair); // True
```

[comment]: <> (Create an `ECDSA` object, using `secp256r1` curve with custom `SHA-512` digest, and create a KeyPair from pre-existing private key.)

[comment]: <> (```java)

[comment]: <> (X9ECParameters curve = SECNamedCurves.getByName&#40;"secp256r1"&#41;;)

[comment]: <> (Digest digest = new SHA512Digest&#40;&#41;;)

[comment]: <> (ECDSA secp256r1 = new ECDSA&#40;curve, digest&#41;;)

[comment]: <> (Binary mySecretKey = Binary.fromBase64&#40;"MHQCAQEEIEa56GG2PTUJyIt4FydaMNItYsjNj6ZIbd7jXvDY4ElfoAcGBSuBBAAKoUQDQgAEJQDn8/vd8oQpA/VE3ch0lM6VAprOTiV9VLp38rwfOog3qUYcTxxX/sxJl1M4HncqEopYIKkkovoFFi62Yph6nw=="&#41;;)

[comment]: <> (KeyPair myKeyPair = secp256r1.keyPairFromSecretKey&#40;mySecretKey&#41;;)

[comment]: <> (```)

Create an `Ed25519` object, create a KeyPair, sign a message and verify it.

```java
Expand All @@ -126,7 +110,19 @@ KeyPair myKeyPair = ed25519.keyPair();
String myMessage = "Hello";
ECDSASignature mySignature = ed25519.signDetached(myMessage, myKeyPair);

ed25519.verify(myMessage, mySignature, myKeyPair) // True
ed25519.verify(myMessage, mySignature, myKeyPair); // True
```

Create a `Falcon` object, create a KeyPair, sign a message and verify it.

```java
Falcon falcon = new Falcon();

KeyPair myKeyPair = falcon.keyPair();
String myMessage = "Hello";
ECDSASignature mySignature = falcon.signDetached(myMessage, myKeyPair);

falcon.verify(myMessage, mySignature, myKeyPair); // True
```

# Hashing
Expand Down
10 changes: 5 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<groupId>com.ltonetwork</groupId>
<artifactId>seasalt</artifactId>

<version>0.0.10</version>
<version>0.0.11</version>
<name>Seasalt</name>
<description>
NaCl and libsodium compatible library for public-key cryptography and hashing using Bouncy Castle.
Expand Down Expand Up @@ -145,11 +145,11 @@
</build>

<dependencies>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.69</version>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.80</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
Expand Down Expand Up @@ -211,4 +211,4 @@
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</project>
</project>
104 changes: 104 additions & 0 deletions src/main/java/com/ltonetwork/seasalt/sign/Falcon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.ltonetwork.seasalt.sign;

import com.ltonetwork.seasalt.KeyPair;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.pqc.crypto.falcon.FalconKeyGenerationParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator;
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconPublicKeyParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.digests.SHAKEDigest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import java.security.SecureRandom;
import java.util.Arrays;

public class Falcon {
private static final FalconParameters FALCON_PARAMS = FalconParameters.falcon_512;

public KeyPair keyPair() {
SecureRandom random = new SecureRandom();
return createKeyPair(random);
}

public KeyPair keyPairFromSeed(byte[] seed) {
byte[] derivedSeed = deriveSeed(seed);
SecureRandom random = new DeterministicRandom(derivedSeed);
return createKeyPair(random);
}

private KeyPair createKeyPair(SecureRandom random) {
FalconKeyPairGenerator keyGen = new FalconKeyPairGenerator();
keyGen.init(new FalconKeyGenerationParameters(random, FALCON_PARAMS));
AsymmetricCipherKeyPair kp = keyGen.generateKeyPair();

byte[] publicKey = ((FalconPublicKeyParameters) kp.getPublic()).getH();
byte[] privateKey = ((FalconPrivateKeyParameters) kp.getPrivate()).getEncoded();
return new KeyPair(publicKey, privateKey);
}

private byte[] deriveSeed(byte[] seed) {
SHAKEDigest shake = new SHAKEDigest(256);
shake.update(seed, 0, seed.length);
byte[] derived = new byte[48]; // Falcon-512 needs at least 48 bytes of entropy
shake.doFinal(derived, 0, derived.length);
return derived;
}

public Signature signDetached(byte[] msg, byte[] privateKey) throws CryptoException {
// Reconstruct the Falcon private key
int len = privateKey.length / 3; // Since pk is not included in getEncoded()
byte[] f = Arrays.copyOfRange(privateKey, 0, len);
byte[] g = Arrays.copyOfRange(privateKey, len, 2 * len);
byte[] F = Arrays.copyOfRange(privateKey, 2 * len, privateKey.length);

FalconPrivateKeyParameters sk = new FalconPrivateKeyParameters(FALCON_PARAMS, f, g, F, null);

FalconSigner signer = new FalconSigner();
signer.init(true, new ParametersWithRandom(sk, new SecureRandom()));

byte[] hash = hashSHA256(msg);
byte[] signature = signer.generateSignature(hash);

return new Signature(signature);
}

public boolean verify(byte[] msg, byte[] signature, byte[] publicKey) {
FalconPublicKeyParameters pk = new FalconPublicKeyParameters(FALCON_PARAMS, publicKey);
FalconSigner verifier = new FalconSigner();
verifier.init(false, pk);

byte[] hash = hashSHA256(msg);

return verifier.verifySignature(hash, signature);
}

private byte[] hashSHA256(byte[] input) {
SHA256Digest digest = new SHA256Digest();
digest.update(input, 0, input.length);
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
return hash;
}

/**
* A deterministic PRNG using SHAKE-256 to replace SecureRandom.
*/
private static class DeterministicRandom extends SecureRandom {
private final byte[] entropy;
private int index = 0;

public DeterministicRandom(byte[] seed) {
this.entropy = seed;
}

@Override
public void nextBytes(byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
bytes[i] = entropy[index++ % entropy.length]; // Loop through entropy deterministically
}
}
}
}
70 changes: 70 additions & 0 deletions src/test/java/sign/FalconTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package sign;

import com.ltonetwork.seasalt.KeyPair;
import com.ltonetwork.seasalt.sign.Falcon;
import com.ltonetwork.seasalt.sign.Signature;
import org.bouncycastle.crypto.CryptoException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.Random;

public class FalconTest {

Falcon falcon;

@BeforeEach
public void init() {
falcon = new Falcon();
}

@Test
public void testKeyPair() {
KeyPair keyPair = falcon.keyPair();
Assertions.assertNotNull(keyPair.getPrivateKey());
Assertions.assertNotNull(keyPair.getPublicKey());
}

@Test
public void testKeyPairFromSeed() {
byte[] seed = new byte[64];
new Random().nextBytes(seed);

KeyPair keyPair = falcon.keyPairFromSeed(seed);

Assertions.assertNotNull(keyPair.getPrivateKey());
Assertions.assertNotNull(keyPair.getPublicKey());
}

@Test
public void testSignDetached() throws CryptoException {
KeyPair keyPair = falcon.keyPair();
byte[] message = "test message".getBytes(StandardCharsets.UTF_8);

Signature signature = falcon.signDetached(message, keyPair.getPrivateKey().getBytes());
Assertions.assertNotNull(signature);
}

@Test
public void testVerify() throws CryptoException {
KeyPair keyPair = falcon.keyPair();
byte[] message = "test message".getBytes(StandardCharsets.UTF_8);

Signature signature = falcon.signDetached(message, keyPair.getPrivateKey().getBytes());
boolean isValid = falcon.verify(message, signature.getBytes(), keyPair.getPublicKey().getBytes());
Assertions.assertTrue(isValid);
}

@Test
public void testVerifyFail() throws CryptoException {
KeyPair keyPair = falcon.keyPair();
byte[] message = "test message".getBytes(StandardCharsets.UTF_8);
byte[] fakeMessage = "wrong message".getBytes(StandardCharsets.UTF_8);

Signature signature = falcon.signDetached(message, keyPair.getPrivateKey().getBytes());
boolean isValid = falcon.verify(fakeMessage, signature.getBytes(), keyPair.getPublicKey().getBytes());
Assertions.assertFalse(isValid);
}
}