diff --git a/pallas-crypto/Cargo.toml b/pallas-crypto/Cargo.toml index 72ceaddd5..b925bbe8a 100644 --- a/pallas-crypto/Cargo.toml +++ b/pallas-crypto/Cargo.toml @@ -20,6 +20,7 @@ thiserror = "1.0" rand_core = "0.9" serde = { version = "1.0.143", features = ["derive"] } pallas-codec = { version = "=1.0.0-alpha.3", path = "../pallas-codec" } +cardano-crypto = { version = "=1.0.6", optional = true, features = ["vrf"] } # kes dependencies rand = { version = "0.9", optional = true } @@ -42,6 +43,7 @@ default = [] sk_clone_enabled = [] kes = ["rand", "rand_chacha", "serde_with", "ed25519-dalek", "zeroize"] relaxed = [] +vrf = ["cardano-crypto/vrf"] [[bench]] harness = false diff --git a/pallas-crypto/README.md b/pallas-crypto/README.md index 2ff38ca3d..d01d5eced 100644 --- a/pallas-crypto/README.md +++ b/pallas-crypto/README.md @@ -8,7 +8,7 @@ Crate with all the cryptographic material to support Cardano protocol: - [x] Ed25519 Extended asymmetric key pair - [ ] Bip32-Ed25519 key derivation - [ ] BIP39 mnemonics -- [x] VRF +- [x] VRF (draft-03 and draft-13, behind the `vrf` feature) - [x] [KES](src/kes/README.md) - [ ] SECP256k1 - [x] Nonce calculations diff --git a/pallas-crypto/proptest-regressions/kes/summed_kes_tests.txt b/pallas-crypto/proptest-regressions/kes/summed_kes_tests.txt new file mode 100644 index 000000000..c2a1abec1 --- /dev/null +++ b/pallas-crypto/proptest-regressions/kes/summed_kes_tests.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc a80811a55e353e6c0a9e2fb8bcc9331942c135baf17fea40c45b258aaa926c09 # shrinks to ((mut sk_bytes,pk),msg1,msg2) = (([118, 121, 140, 51, 101, 51, 163, 103, 235, 28, 143, 46, 23, 39, 102, 159, 3, 166, 178, 183, 145, 159, 199, 149, 221, 188, 177, 156, 251, 0, 120, 48, 26, 41, 81, 47, 50, 158, 95, 189, 47, 46, 196, 91, 131, 108, 2, 69, 219, 255, 46, 128, 103, 108, 82, 236, 55, 105, 210, 57, 189, 70, 145, 169, 196, 71, 254, 141, 70, 197, 189, 242, 7, 216, 170, 28, 166, 128, 195, 49, 110, 62, 186, 177, 58, 103, 6, 164, 62, 56, 83, 82, 19, 166, 0, 156, 51, 17, 32, 79, 87, 94, 90, 106, 181, 195, 124, 188, 72, 148, 114, 132, 89, 83, 191, 241, 63, 78, 85, 37, 245, 98, 65, 169, 129, 108, 136, 138, 227, 102, 43, 118, 77, 163, 227, 112, 111, 204, 101, 26, 132, 181, 91, 131, 95, 211, 240, 225, 115, 131, 210, 195, 222, 146, 15, 227, 58, 62, 69, 23, 92, 184, 60, 255, 172, 115, 30, 174, 92, 25, 25, 10, 40, 23, 228, 203, 28, 162, 69, 142, 77, 128, 68, 70, 134, 253, 221, 13, 45, 94, 115, 0, 166, 116, 181, 77, 120, 117, 36, 43, 166, 150, 140, 27, 108, 4, 184, 88, 27, 66, 119, 83, 102, 46, 190, 150, 165, 223, 56, 55, 50, 237, 91, 240, 137, 156, 122, 26, 49, 15, 29, 121, 240, 215, 65, 111, 191, 183, 200, 195, 53, 46, 187, 218, 178, 24, 56, 96, 21, 143, 122, 149, 50, 193, 122, 142, 192, 181, 155, 199, 52, 67, 29, 6, 215, 250, 215, 137, 227, 42, 248, 102, 118, 247, 59, 254, 229, 1, 134, 60, 234, 80, 210, 235, 252, 36, 99, 68, 101, 170, 146, 97, 6, 6, 6, 8, 71, 251, 64, 179, 144, 98, 170, 131, 152, 111, 252, 127, 150, 161, 80, 99, 224, 46, 25, 173, 170, 30, 13, 146, 50, 51, 155, 190, 232, 53, 157, 79, 209, 109, 188, 2, 37, 8, 75, 19, 46, 248, 55, 97, 19, 212, 93, 101, 5, 41, 197, 211, 117, 220, 28, 10, 230, 84, 248, 143, 30, 254, 217, 46, 235, 222, 52, 72, 247, 200, 114, 220, 48, 96, 222, 97, 126, 187, 52, 197, 89, 159, 105, 244, 8, 31, 121, 106, 94, 182, 48, 63, 193, 12, 250, 28, 93, 73, 235, 141, 218, 11, 190, 114, 29, 94, 16, 184, 88, 218, 174, 195, 109, 171, 3, 198, 76, 215, 3, 193, 74, 212, 49, 128, 2, 99, 132, 165, 11, 107, 152, 165, 64, 4, 248, 95, 122, 88, 142, 35, 184, 216, 181, 92, 188, 74, 42, 2, 116, 113, 168, 155, 246, 221, 110, 206, 41, 26, 247, 173, 26, 154, 132, 215, 31, 96, 184, 163, 125, 27, 100, 7, 205, 65, 160, 111, 123, 166, 4, 10, 112, 94, 185, 249, 201, 76, 248, 194, 123, 81, 226, 130, 128, 139, 72, 235, 214, 229, 222, 86, 247, 76, 249, 76, 108, 243, 242, 249, 136, 25, 130, 65, 102, 240, 24, 89, 118, 111, 138, 76, 90, 115, 25, 128, 20, 233, 207, 172, 30, 210, 124, 94, 254, 143, 172, 205, 186, 45, 228, 77, 247, 91, 171, 180, 204, 201, 221, 214, 119, 144, 27, 239, 128, 22, 52, 169, 231, 58, 209, 94, 15, 76, 65, 213, 60, 253, 166, 101, 7, 83, 225, 251, 95, 198, 184, 65, 33, 52, 106, 110, 190, 165, 83, 46, 152, 181, 18, 232, 101, 90, 154, 191, 9, 3, 213, 99, 48, 208, 136, 233, 142, 21, 72, 14, 124, 213, 191, 4, 13, 140, 19, 71, 0, 0, 0, 0], PublicKey([220, 226, 205, 176, 140, 133, 89, 107, 176, 150, 130, 165, 8, 119, 148, 63, 96, 138, 43, 31, 154, 151, 32, 236, 113, 152, 197, 198, 142, 158, 38, 162])), [], []) diff --git a/pallas-crypto/src/kes/README.md b/pallas-crypto/src/kes/README.md index 6f94a0e58..6245877e1 100644 --- a/pallas-crypto/src/kes/README.md +++ b/pallas-crypto/src/kes/README.md @@ -11,7 +11,8 @@ This library defines macros to generate KES algorithms with different depths. We algorithms up to depth 7. However, if you require a higher depth key, feel free to open an issue/PR. -This module requires the `kes` feature flag. +This module requires the `kes` feature flag. VRF support (draft-03 and draft-13) now lives +alongside KES under the `vrf` feature; Cardano currently uses KES Sum6 and VRF draft-03. ## Library usage diff --git a/pallas-crypto/src/kes/summed_kes_tests.rs b/pallas-crypto/src/kes/summed_kes_tests.rs index c26767859..8eeb370d9 100644 --- a/pallas-crypto/src/kes/summed_kes_tests.rs +++ b/pallas-crypto/src/kes/summed_kes_tests.rs @@ -47,6 +47,8 @@ mod test { #[test] fn two_msgs_have_different_signature_with_one_skey(((mut sk_bytes,_pk),msg1,msg2) in (secret_public_key_bytes(), payload(),payload())) { + prop_assume!(msg1 != msg2); + let mut sk_bytes1 = [0u8; Sum6Kes::SIZE + 4]; sk_bytes1.copy_from_slice(&sk_bytes); let sk = Sum6Kes::from_bytes(&mut sk_bytes); @@ -70,6 +72,8 @@ mod test { #[test] fn simple_verification_fails_for_other_msg(((mut sk_bytes,pk),msg1,msg2) in (secret_public_key_bytes(), payload(), payload())) { + prop_assume!(msg1 != msg2); + let sk = Sum6Kes::from_bytes(&mut sk_bytes); let sig = sk?.sign(&msg1); let err_str = String::from("signature error: Verification equation was not satisfied"); diff --git a/pallas-crypto/src/lib.rs b/pallas-crypto/src/lib.rs index 0533499f4..0c2459e11 100644 --- a/pallas-crypto/src/lib.rs +++ b/pallas-crypto/src/lib.rs @@ -5,3 +5,5 @@ pub mod kes; pub mod key; pub mod memsec; pub mod nonce; +#[cfg(feature = "vrf")] +pub mod vrf; diff --git a/pallas-crypto/src/vrf/mod.rs b/pallas-crypto/src/vrf/mod.rs new file mode 100644 index 000000000..d76a6f80e --- /dev/null +++ b/pallas-crypto/src/vrf/mod.rs @@ -0,0 +1,101 @@ +//! Verifiable Random Functions (VRF) bindings +//! +//! This module re-exports Cardano-compatible VRF primitives from the `cardano-crypto` +//! crate so downstream users can generate proofs and verify outputs for both +//! draft-03 (Cardano standard) and draft-13 variants. + +use cardano_crypto::common::{CryptoError, CryptoResult}; +use cardano_crypto::vrf::CertifiedVrf; + +pub use cardano_crypto::vrf::{ + cardano_compat::cardano_vrf_prove as prove_cardano, + cardano_compat::cardano_vrf_verify as verify_cardano, CertifiedVrf as CertifiedOutput, + OutputVrf as Output, VrfDraft03, VrfDraft13, VrfKeyPair, VrfProof, VrfSigningKey, + VrfVerificationKey, DRAFT03_PROOF_SIZE, DRAFT13_PROOF_SIZE, OUTPUT_SIZE, PUBLIC_KEY_SIZE, + SECRET_KEY_SIZE, SEED_SIZE, +}; + +/// Generate a draft-03 keypair from a 32-byte seed. +pub fn keypair_from_seed(seed: &[u8; SEED_SIZE]) -> VrfKeyPair { + VrfKeyPair::generate(seed) +} + +/// Produce a draft-03 VRF proof and output for the given message. +pub fn prove_draft03(sk: &VrfSigningKey, message: &[u8]) -> CryptoResult<(VrfProof, Output)> { + let proof = VrfDraft03::prove(sk, message)?; + let output = VrfDraft03::proof_to_hash(&proof)?; + Ok((proof, Output::new(output))) +} + +/// Verify a draft-03 VRF proof and return the output if valid. +pub fn verify_draft03( + vk: &VrfVerificationKey, + proof: &VrfProof, + message: &[u8], +) -> CryptoResult { + let output = VrfDraft03::verify(vk, proof, message)?; + Ok(Output::new(output)) +} + +/// Convenience wrapper that runs draft-03 end-to-end and returns a certified output. +pub fn certify_draft03(sk: &VrfSigningKey, message: &[u8]) -> CryptoResult { + CertifiedVrf::eval(sk, message) +} + +/// Verify a certified draft-03 output. +pub fn verify_certified( + vk: &VrfVerificationKey, + certified: &CertifiedOutput, + message: &[u8], +) -> CryptoResult<()> { + certified.verify(vk, message) +} + +/// Produce a draft-13 VRF proof and output for the given message. +pub fn prove_draft13( + sk: &VrfSigningKey, + message: &[u8], +) -> CryptoResult<([u8; cardano_crypto::vrf::draft13::PROOF_SIZE], Output)> { + let proof = VrfDraft13::prove(sk, message)?; + let output = VrfDraft13::proof_to_hash(&proof)?; + Ok((proof, Output::new(output))) +} + +/// Verify a draft-13 VRF proof and return the output if valid. +pub fn verify_draft13( + vk: &VrfVerificationKey, + proof: &[u8; cardano_crypto::vrf::draft13::PROOF_SIZE], + message: &[u8], +) -> CryptoResult { + let output = VrfDraft13::verify(vk, proof, message)?; + Ok(Output::new(output)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn draft03_roundtrip() { + let seed = [7u8; SEED_SIZE]; + let (proof, output) = + prove_draft03(&keypair_from_seed(&seed).signing_key, b"msg").expect("prove"); + let vk = keypair_from_seed(&seed).verification_key; + let out2 = verify_draft03(&vk, &proof, b"msg").expect("verify"); + assert_eq!(output.as_bytes(), out2.as_bytes()); + } + + #[test] + fn draft13_roundtrip() { + let seed = [9u8; SEED_SIZE]; + let kp = keypair_from_seed(&seed); + let (proof, output) = prove_draft13(&kp.signing_key, b"msg-13").expect("prove13"); + let out2 = verify_draft13(&kp.verification_key, &proof, b"msg-13").expect("verify13"); + assert_eq!(output.as_bytes(), out2.as_bytes()); + } +} + +/// Error type alias for VRF operations. +pub type Error = CryptoError; +/// Result type alias for VRF operations. +pub type Result = CryptoResult; diff --git a/pallas-crypto/tests/kes_golden.rs b/pallas-crypto/tests/kes_golden.rs new file mode 100644 index 000000000..f90d4d8a7 --- /dev/null +++ b/pallas-crypto/tests/kes_golden.rs @@ -0,0 +1,68 @@ +#![cfg(feature = "kes")] + +use pallas_crypto::kes::summed_kes::Sum6Kes; +use pallas_crypto::kes::traits::{KesSig, KesSk}; + +#[test] +fn sum6_total_periods_and_roundtrip() { + let mut key_bytes = [0u8; Sum6Kes::SIZE + 4]; + let mut seed = [0x46u8; 32]; + let (mut sk, vk) = Sum6Kes::keygen(&mut key_bytes, &mut seed); + + // sign at period 0 + let msg0 = b"Sum6 period 0"; + let sig0 = sk.sign(msg0); + sig0.verify(sk.get_period(), &vk, msg0).expect("verify p0"); + + // evolve to final period and sign + for _ in 0..63 { + sk.update().expect("evolve"); + } + assert_eq!(sk.get_period(), 63); + + let msg_last = b"Sum6 period 63"; + let sig_last = sk.sign(msg_last); + sig_last.verify(63, &vk, msg_last).expect("verify p63"); + + // adjacent periods should fail + assert!(sig_last.verify(62, &vk, msg_last).is_err()); +} + +#[test] +fn sum6_verification_key_stability() { + let mut key_bytes = [0u8; Sum6Kes::SIZE + 4]; + let mut seed = [0x99u8; 32]; + let (mut sk, vk0) = Sum6Kes::keygen(&mut key_bytes, &mut seed); + let vk0_bytes = vk0.as_bytes().to_vec(); + + for period in 0..10 { + let vk_bytes = sk.to_pk().as_bytes().to_vec(); + assert_eq!( + vk0_bytes, vk_bytes, + "vkey must stay stable at period {period}" + ); + if period < 9 { + sk.update().expect("evolve"); + } + } +} + +#[test] +fn sum6_deterministic_from_seed() { + let seed = [0xCCu8; 32]; + + let mut key_bytes1 = [0u8; Sum6Kes::SIZE + 4]; + let mut seed1 = seed.clone(); + let (sk1, vk1) = Sum6Kes::keygen(&mut key_bytes1, &mut seed1); + + let mut key_bytes2 = [0u8; Sum6Kes::SIZE + 4]; + let mut seed2 = seed.clone(); + let (sk2, vk2) = Sum6Kes::keygen(&mut key_bytes2, &mut seed2); + + let m = b"deterministic"; + let sig1 = sk1.sign(m); + let sig2 = sk2.sign(m); + + assert_eq!(vk1.as_bytes(), vk2.as_bytes()); + assert_eq!(sig1.to_bytes(), sig2.to_bytes()); +} diff --git a/pallas-crypto/tests/vrf_golden.rs b/pallas-crypto/tests/vrf_golden.rs new file mode 100644 index 000000000..9ce63390c --- /dev/null +++ b/pallas-crypto/tests/vrf_golden.rs @@ -0,0 +1,117 @@ +#![cfg(feature = "vrf")] + +use pallas_crypto::vrf::{keypair_from_seed, verify_draft03, VrfDraft03}; + +fn hex_decode(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} + +#[test] +fn draft03_ietf_vector_10() { + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-vrf-03#appendix-A.1 + let sk_seed = hex_decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"); + let expected_pk = + hex_decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); + let expected_pi = hex_decode(concat!( + "b6b4699f87d56126c9117a7da55bd0085246f4c56dbc95d20172612e9d38e8d7", + "ca65e573a126ed88d4e30a46f80a666854d675cf3ba81de0de043c3774f06156", + "0f55edc256a787afe701677c0f602900", + )); + let expected_beta = hex_decode(concat!( + "5b49b554d05c0cd5a5325376b3387de59d924fd1e13ded44648ab33c21349a60", + "3f25b84ec5ed887995b33da5e3bfcb87cd2f64521c4c62cf825cffabbe5d31cc", + )); + + let seed: [u8; 32] = sk_seed.try_into().unwrap(); + let (sk, pk) = VrfDraft03::keypair_from_seed(&seed); + + assert_eq!(pk.as_slice(), expected_pk.as_slice()); + + let proof = VrfDraft03::prove(&sk, &[]).expect("prove"); + assert_eq!(proof.as_slice(), expected_pi.as_slice()); + + let beta = VrfDraft03::verify(&pk, &proof, &[]).expect("verify"); + assert_eq!(beta.as_slice(), expected_beta.as_slice()); + + let beta2 = VrfDraft03::proof_to_hash(&proof).expect("proof_to_hash"); + assert_eq!(beta, beta2); +} + +#[test] +fn draft03_ietf_vector_11() { + let sk_seed = hex_decode("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"); + let expected_pk = + hex_decode("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"); + let alpha = hex_decode("72"); + let expected_pi = hex_decode("ae5b66bdf04b4c010bfe32b2fc126ead2107b697634f6f7337b9bff8785ee111200095ece87dde4dbe87343f6df3b107d91798c8a7eb1245d3bb9c5aafb093358c13e6ae1111a55717e895fd15f99f07"); + let expected_beta = hex_decode(concat!( + "94f4487e1b2fec954309ef1289ecb2e15043a2461ecc7b2ae7d4470607ef82eb", + "1cfa97d84991fe4a7bfdfd715606bc27e2967a6c557cfb5875879b671740b7d8", + )); + + let seed: [u8; 32] = sk_seed.try_into().unwrap(); + let (sk, pk) = VrfDraft03::keypair_from_seed(&seed); + + assert_eq!(pk.as_slice(), expected_pk.as_slice()); + + let proof = VrfDraft03::prove(&sk, &alpha).expect("prove"); + assert_eq!( + proof.as_slice().len(), + pallas_crypto::vrf::DRAFT03_PROOF_SIZE + ); + assert_eq!(proof.as_slice(), expected_pi.as_slice()); + + let beta = VrfDraft03::verify(&pk, &proof, &alpha).expect("verify"); + assert_eq!(beta.as_slice().len(), pallas_crypto::vrf::OUTPUT_SIZE); + assert_eq!(beta.as_slice(), expected_beta.as_slice()); +} + +#[test] +fn draft03_ietf_vector_12() { + let sk_seed = hex_decode("c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7"); + let expected_pk = + hex_decode("fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"); + let alpha = hex_decode("af82"); + let expected_pi = hex_decode(concat!( + "dfa2cba34b611cc8c833a6ea83b8eb1bb5e2ef2dd1b0c481bc42ff36ae7847f6", + "ab52b976cfd5def172fa412defde270c8b8bdfbaae1c7ece17d9833b1bcf3106", + "4fff78ef493f820055b561ece45e1009", + )); + let expected_beta = hex_decode(concat!( + "2031837f582cd17a9af9e0c7ef5a6540e3453ed894b62c293686ca3c1e319dde", + "9d0aa489a4b59a9594fc2328bc3deff3c8a0929a369a72b1180a596e016b5ded", + )); + + let seed: [u8; 32] = sk_seed.try_into().unwrap(); + let (sk, pk) = VrfDraft03::keypair_from_seed(&seed); + + assert_eq!(pk.as_slice(), expected_pk.as_slice()); + + let proof = VrfDraft03::prove(&sk, &alpha).expect("prove"); + assert_eq!(proof.as_slice(), expected_pi.as_slice()); + + let beta = VrfDraft03::verify(&pk, &proof, &alpha).expect("verify"); + assert_eq!(beta.as_slice(), expected_beta.as_slice()); +} + +#[test] +fn draft03_cardano_messages() { + let seed = [0x42u8; 32]; + let kp = keypair_from_seed(&seed); + let messages: &[&[u8]] = &[ + b"Block header hash", + b"Epoch nonce derivation", + b"Leader election slot 12345", + &[0u8; 64], + &[], + ]; + + for msg in messages { + let (proof, out1) = pallas_crypto::vrf::prove_draft03(&kp.signing_key, msg).expect("prove"); + let out2 = verify_draft03(&kp.verification_key, &proof, msg).expect("verify"); + assert_eq!(out1.as_bytes(), out2.as_bytes()); + } +} diff --git a/pallas/Cargo.toml b/pallas/Cargo.toml index 3bf83feba..6f1f443fa 100644 --- a/pallas/Cargo.toml +++ b/pallas/Cargo.toml @@ -30,3 +30,4 @@ unstable = ["hardano", "pallas-traverse/unstable"] # pallas-validate feature flags phase2 = ["pallas-validate/phase2"] relaxed = ["pallas-primitives/relaxed", "pallas-crypto/relaxed"] +vrf = ["pallas-crypto/vrf"]