Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

schnorr: Adds Schnorr implementation for upcoming EIP #26

Merged
merged 19 commits into from
Sep 21, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/solc-version-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
- name: Run forge tests against lowest and highest supported solc version
run: >
forge test --use 0.8.16 &&
forge test --use 0.8.26
forge test --use 0.8.27
18 changes: 15 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ clean: ## Clean build artifacts

.PHONY: test
test: ## Run full test suite
@forge test
@forge test --show-progress

.PHONY: test-intense
test-intense: ## Run full test suite with intense fuzzing
@FOUNDRY_PROFILE=intense forge test
@FOUNDRY_PROFILE=intense forge test --show-progress

.PHONY: test-summary
test-summary: ## Print summary of test suite
@forge test --summary
@forge test --summary --show-progress

.PHONY: coverage
coverage: ## Update coverage report and open lcov web interface
Expand Down Expand Up @@ -62,6 +62,18 @@ examples: ## Run examples
@echo "##"
@echo "########################################"
@forge script examples/secp256r1/Secp256r1.sol:Secp256r1Example -v
@echo "########################################"
@echo "##"
@echo "## ECDSA on secp56k1"
@echo "##"
@echo "########################################"
@forge script examples/secp256k1/signatures/ECDSA.sol:ECDSAExample -v
@echo "########################################"
@echo "##"
@echo "## Schnorr (ERC-XXX)"
@echo "##"
@echo "########################################"
@forge script examples/secp256k1/signatures/Schnorr.sol:SchnorrExample -v

.PHONY: fmt
fmt: ## Format project
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ $ forge install verklegarden/crysol
## Examples

Several examples are provided in [`examples/`](./examples), such as:
- secure key pair and Ethereum address creation
- secure key pair and Ethereum address generation
- secp256k1 point arithmetic
- Schnorr and ECDSA signature creation and verification

Expand Down
27 changes: 27 additions & 0 deletions docs/Intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,30 @@ modifier vmed() {
// forgefmt: disable-end
// ~~~~~~~~~~~~~~~~~~~~~~~
```

## TODO: Signature Rules

- Malleable signatures are deemed invalid
- For ECDSA malleable signature if `s > Q/2` which is in sync with ecosystem
- Only 32 byte digests are signed, never byte strings
- It is always assumed that the digest is a keccak256 hash digest
- The default `sign` function MUST only sign domain separated messages
- For ECDSA using "Ethereum Signed Message" as defined via `eth_sign`
- For Schnorr its part of ERC-XXX
- There are `signRaw` functions that MUST NOT domain separate user input digests
- Usage is discouraged
- User input is called `digest`, which is domain separated to message `m`, wich is signed

- Terminology:
- A user wants to sign byte string `message`
- The `message` is hashed via keccak256 to `digest`
- The `digest` is domain separated to `m`

## TODO: Sanity Check Rules

- De/Serialization functions MUST revert if object is invalid/insane
- Example: It MUST NOT be possible to serialize an invalid public key
- Example: It MUST NOT be possible to deserialize a malleable ECDSA signature
- Type conversion functions MUST NOT revert if object is invalid/insane
- Example: It MUST be possible to transform an invalid public key to an point
- Example: It MUST be possible to transform an insance Schnorr signature to a compressed Schnorr signature
164 changes: 0 additions & 164 deletions docs/secp256k1/signatures/Schnorr.md

This file was deleted.

39 changes: 31 additions & 8 deletions examples/secp256k1/signatures/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,43 @@ contract ECDSAExample is Script {
using ECDSA for Signature;

function run() public {
bytes memory message = bytes("crysol <3");

// Create new cryptographically sound secret key.
// Create new cryptographically sound secret key and respective
// public key and address.
SecretKey sk = Secp256k1Offchain.newSecretKey();
// assert(sk.isValid());
PublicKey memory pk = sk.toPublicKey();
address addr = pk.toAddress();

// Create digest of the message to sign.
//
// crysol's sign() functions only accept bytes32 digests to enforce
// static payload size.
bytes32 digest = keccak256(bytes("crysol <3"));

// Note that crysol's sign() function domain separates input digests.
// The actual message being signed can be constructed via:
bytes32 m = ECDSA.constructMessageHash(digest);

// Sign message via ECDSA.
Signature memory sig = sk.sign(message);
// Sign digest via ECDSA.
Signature memory sig = sk.sign(digest);
console.log("Signed message via ECDSA, signature:");
console.log(sig.toString());
console.log("");

// It's also possible to use the low-level signRaw() function to not
// domain separate the input digest.
// However, usage is discouraged.
Signature memory sig2 = sk.signRaw(m);

// Note that crysol uses RFC-6979 to construct deterministic ECDSA
// nonces and thereby signatures. Therefore, the two signatures are
// expected to be equal.
assert(sig.v == sig2.v);
assert(sig.r == sig2.r);
assert(sig.s == sig2.s);

// Verify signature via public key or address.
// assert(sk.toPublicKey().verify(message, sig));
// assert(sk.toPublicKey().toAddress().verify(message, sig));
assert(pk.verify(m, sig));
assert(addr.verify(m, sig));

// Default serialization (65 bytes).
console.log("Default encoded signature:");
Expand All @@ -62,5 +84,6 @@ contract ECDSAExample is Script {
// EIP-2098 serialization (64 bytes).
console.log("EIP-2098 (compact) encoded signature:");
console.logBytes(sig.toCompactEncoded());
console.log("");
}
}
60 changes: 49 additions & 11 deletions examples/secp256k1/signatures/Schnorr.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
import {SchnorrOffchain} from
"src/offchain/secp256k1/signatures/SchnorrOffchain.sol";
import {
Schnorr, Signature
Schnorr,
Signature,
SignatureCompressed
} from "src/onchain/secp256k1/signatures/Schnorr.sol";

/**
Expand All @@ -32,28 +34,64 @@ contract SchnorrExample is Script {
using Secp256k1 for SecretKey;
using Secp256k1 for PublicKey;

using SchnorrOffchain for Signature;
using SchnorrOffchain for SecretKey;
using SchnorrOffchain for PublicKey;
using SchnorrOffchain for Signature;
using SchnorrOffchain for SignatureCompressed;
using Schnorr for SecretKey;
using Schnorr for PublicKey;
using Schnorr for Signature;
using Schnorr for SignatureCompressed;

function run() public {
bytes memory message = bytes("crysol <3");

// Create a cryptographically secure secret key.
// Create new cryptographically sound secret key and respective
// public key.
SecretKey sk = Secp256k1Offchain.newSecretKey();
// assert(sk.isValid());
PublicKey memory pk = sk.toPublicKey();

// Create digest of the message to sign.
//
// crysol's sign() functions only accept bytes32 digests to enforce
// static payload size.
bytes32 digest = keccak256(bytes("crysol <3"));

// Create Schnorr signature.
Signature memory sig = sk.sign(message);
// assert(!sig.isMalleable());
// Note that crysol's sign() function domain separates input digests.
// The actual message being signed can be constructed via:
bytes32 m = Schnorr.constructMessageHash(digest);

// Sign digest via Schnorr.
Signature memory sig = sk.sign(digest);
console.log("Signed message via Schnorr, signature:");
console.log(sig.toString());
console.log("");

// Verify signature.
// assert(sk.toPublicKey().verify(message, sig));
// Note that Schnorr signatures can be compressed too.
SignatureCompressed memory sigComp = sig.toCompressed();
console.log("Compressed Schnorr signature:");
console.log(sigComp.toString());
console.log("");

// It's also possible to use the low-level signRaw() function to not
// domain separate the input digest.
// However, usage is discouraged.
Signature memory sig2 = sk.signRaw(m);

// Note that crysol uses random nonces to construct Schnorr signatures.
// Therefore, the two signatures are expected to not be equal.
assert(sig.s != sig2.s);
assert(!sig.r.eq(sig2.r));

// Verify signature via public key.
assert(pk.verify(m, sig));

// Default serialization (96 bytes).
console.log("Default encoded signature:");
console.logBytes(sig.toEncoded());
console.log("");

// Compressed serialization (52 bytes).
console.log("Compressed Schnorr signature:");
console.logBytes(sig.toCompressedEncoded());
console.log("");
}
}
Loading
Loading