diff --git a/Cargo.lock b/Cargo.lock index 7f99bff0fa..6a6f69ac0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6753,6 +6753,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "sign" +version = "0.1.0" +dependencies = [ + "bech32 0.8.1", + "chain-addr", + "chain-core", + "chain-crypto", + "chain-impl-mockchain", + "chain-ser", + "chain-storage", + "chain-vote", + "clap 4.4.6", + "clap_complete_command", + "color-eyre", + "cryptoxide 0.4.4", + "csv", + "ed25519-dalek", + "hex", + "jormungandr-lib", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.5.1", + "serde", + "serde_json", + "serde_yaml", + "thiserror", +] + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/Cargo.toml b/Cargo.toml index 4ca82bd472..4701c17a0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "src/vit-servicing-station-f10/vit-servicing-station-lib-f10", "src/vit-servicing-station-f10/vit-servicing-station-server-f10", "src/vit-servicing-station-f10/vit-servicing-station-tests-f10", + "src/sign", ] [workspace.dependencies] diff --git a/src/chain-libs/chain-impl-mockchain/src/transaction/transaction.rs b/src/chain-libs/chain-impl-mockchain/src/transaction/transaction.rs index 6b147493f8..f45e7e7271 100644 --- a/src/chain-libs/chain-impl-mockchain/src/transaction/transaction.rs +++ b/src/chain-libs/chain-impl-mockchain/src/transaction/transaction.rs @@ -265,6 +265,7 @@ pub(super) struct TransactionStruct { /// Verify the structure of the transaction and return all the offsets fn get_spine(slice: &[u8]) -> Result { let sz = slice.len(); + let mut codec = Codec::new(slice); // read payload diff --git a/src/chain-libs/chain-vote/src/cryptography/zkps/unit_vector/zkp.rs b/src/chain-libs/chain-vote/src/cryptography/zkps/unit_vector/zkp.rs index 914114bb3f..d5d2369c73 100644 --- a/src/chain-libs/chain-vote/src/cryptography/zkps/unit_vector/zkp.rs +++ b/src/chain-libs/chain-vote/src/cryptography/zkps/unit_vector/zkp.rs @@ -293,6 +293,17 @@ impl Zkp { self.ibas.iter() } + /// Return announcement commitments group elements + pub fn announcments_group_elements(&self) -> Vec { + let mut announcements = Vec::new(); + for g in self.ibas.clone() { + announcements.push(g.i); + announcements.push(g.b); + announcements.push(g.a) + } + announcements + } + /// Return an iterator of the encryptions of the polynomial coefficients pub fn ds(&self) -> impl Iterator { self.ds.iter() @@ -303,6 +314,18 @@ impl Zkp { self.zwvs.iter() } + /// Return an iterator of the response related to the randomness + pub fn response_randomness_group_elements(&self) -> Vec { + let mut response = Vec::new(); + for z in self.zwvs.iter().clone() { + response.push(z.z.clone()); + response.push(z.w.clone()); + response.push(z.v.clone()); + } + + response + } + /// Return R pub fn r(&self) -> &Scalar { &self.r diff --git a/src/chain-libs/chain-vote/src/encrypted_vote.rs b/src/chain-libs/chain-vote/src/encrypted_vote.rs index f8a13fda76..79bda44fc6 100644 --- a/src/chain-libs/chain-vote/src/encrypted_vote.rs +++ b/src/chain-libs/chain-vote/src/encrypted_vote.rs @@ -219,6 +219,7 @@ mod tests { #[test] fn unit_vector() { let uv = UnitVector::new(5, 0).unwrap(); + assert_eq!( &uv.iter().collect::>()[..], [true, false, false, false, false] diff --git a/src/sign/Cargo.toml b/src/sign/Cargo.toml new file mode 100644 index 0000000000..c84fd72d20 --- /dev/null +++ b/src/sign/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "sign" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chain-crypto = { path = "../chain-libs/chain-crypto" } +chain-vote = { path = "../chain-libs/chain-vote" } +jormungandr-lib = { path = "../jormungandr/jormungandr-lib" } +chain-addr = { path = "../chain-libs/chain-addr" } +chain-core = { path = "../chain-libs/chain-core" } +chain-impl-mockchain = { path = "../chain-libs/chain-impl-mockchain" ,features= ["audit"]} +chain-ser = { path = "../chain-libs/chain-ser" } +chain-storage = { path = "../chain-libs/chain-storage" } + + +hex = "0.4" +cryptoxide = "0.4.2" +rand_chacha = "0.3" + +clap = { version = "4", features = ["derive", "cargo"] } +clap_complete_command = { version = "0.5" } + +color-eyre = "0.6" +thiserror = "1.0.40" +csv = "1.1" + +serde = "1.0" +serde_json = "1.0" +serde_yaml = "0.8.17" +rand = "0.8.3" +bech32 = "0.8" + + +rand_core = { version = "0.5.1", default-features = false } + + +ed25519-dalek = "1.0.1" \ No newline at end of file diff --git a/src/sign/README.md b/src/sign/README.md new file mode 100644 index 0000000000..199360735a --- /dev/null +++ b/src/sign/README.md @@ -0,0 +1,34 @@ +# Fragment generator and signer: + +## Specifications + [*see here for format.abnf*](../chain-libs/chain-impl-mockchain/doc/format.abnf) + + [*see here for format.md*](../chain-libs/chain-impl-mockchain/doc/format.md) + +## Ingredients for generating a fragment + +- Election public key +- Alice public key +- Alice private key +- proposal to vote on +- vote plan id (hash of voteplan) + +*Example usage:* + +``` +cargo build --release -p sign +``` + +*Generate raw fragment in byte representation* + +```bash + +ELECTION_PUB_KEY=ristretto255_votepk1ppxnuxrqa4728evnp2ues000uvwvwtxmtf77ejc29lknjuqqu44s4cfmja +ALICE_SK=56e367979579e2ce27fbd305892b0706b7dede999a534a864a7430a5c6aefd3c +ALICE_PK=ea084d2d80ed0ab681333d934efc56df3868d13d46a2de3b7f27f40b62e5344d +PROPOSAL=5 +VOTE_PLAN_ID=36ad42885189a0ac3438cdb57bc8ac7f6542e05a59d1f2e4d1d38194c9d4ac7b + +./target/release/sign --election-pub-key $ELECTION_PUB_KEY --private-key $ALICE_SK --public-key $ALICE_PK --proposal $PROPOSAL --vote-plan-id $VOTE_PLAN_ID + +``` \ No newline at end of file diff --git a/src/sign/src/fragment.rs b/src/sign/src/fragment.rs new file mode 100644 index 0000000000..b50d928ebe --- /dev/null +++ b/src/sign/src/fragment.rs @@ -0,0 +1,323 @@ +//! Generate Fragments based upon specification +//! Reference specfication for more context in relation to constants outlined in this file. + +use chain_ser::packer::Codec; + +use chain_vote::{Ciphertext, ProofOfCorrectVote}; +use ed25519_dalek::{ed25519::signature::Signature, *}; +use std::error; + +/// Payload type = 2 +/// %x02 ENCRYPTED-VOTE PROOF-VOTE ; Private payload +const ENCRYPTED_PAYLOAD: u8 = 2; + +/// VoteCast tag +const VOTE_CAST_TAG: u8 = 11; + +/// INPUT-ACCOUNT = %xff VALUE UNTAG-ACCOUNT-ID +const INPUT_ACCOUNT: u8 = 255; + +/// Block epoch + slot +/// This is redundant as time checks have been removed +const EPOCH: u32 = 0; +const SLOT: u32 = 0; + +/// Only 1 input (subsequently 1 witness), no output +/// VoteCast TX should have only 1 input, 0 output and 1 witness (signature). +const INPUT: u8 = 1; +const OUTPUT: u8 = 0; + +/// Nonce +const NONCE: u32 = 0; + +/// Type = 2 +/// utxo witness scheme +/// ED25519 Signature (64 bytes) +const WITNESS_SCHEME: u8 = 2; + +/// Padding +const PADDING: u8 = 0; + +/// Values in inputs: redundant for voting +const VALUE: u64 = 0; + +/// Padding and Tag are 1 byte each; size must be added to the fragment size +const PADDING_AND_TAG_SIZE: u32 = 2; + +/// Generate vote fragment in bytes +pub fn generate_vote_fragment( + keypair: Keypair, + encrypted_vote: Vec, + proof: Vec, + proposal: u8, + vote_plan_id: &[u8], +) -> Result, Box> { + let mut vote_cast = Codec::new(Vec::new()); + + vote_cast.put_bytes(vote_plan_id)?; + vote_cast.put_u8(proposal)?; + vote_cast.put_u8(ENCRYPTED_PAYLOAD)?; + vote_cast.put_bytes(&encrypted_vote)?; + vote_cast.put_bytes(&proof)?; + + let data_to_sign = vote_cast.into_inner().clone(); + + let (inputs, witness) = compose_inputs_and_witnesses(keypair, data_to_sign.clone())?; + + let mut vote_cast = Codec::new(Vec::new()); + vote_cast.put_bytes(&data_to_sign)?; + vote_cast.put_bytes(&inputs)?; + vote_cast.put_bytes(&witness)?; + + let data = vote_cast.into_inner(); + + // prepend msg with size of fragment msg + let mut vote_cast = Codec::new(Vec::new()); + vote_cast.put_be_u32(data.len() as u32 + PADDING_AND_TAG_SIZE)?; + vote_cast.put_u8(PADDING)?; + vote_cast.put_u8(VOTE_CAST_TAG)?; + vote_cast.put_bytes(data.as_slice())?; + + Ok(vote_cast.into_inner()) +} + +/// Generate Inputs-Outputs-Witnesses in bytes +fn compose_inputs_and_witnesses( + keypair: Keypair, + data_to_sign: Vec, +) -> Result<(Vec, Vec), Box> { + let mut inputs = Codec::new(Vec::new()); + + inputs.put_be_u32(EPOCH)?; + inputs.put_be_u32(SLOT)?; + inputs.put_u8(INPUT)?; + inputs.put_u8(OUTPUT)?; + + inputs.put_u8(INPUT_ACCOUNT)?; + inputs.put_be_u64(VALUE)?; + inputs.put_bytes(keypair.public.as_bytes())?; + let inputs = inputs.into_inner().clone(); + + let mut tx_data_to_sign = Codec::new(Vec::new()); + tx_data_to_sign.put_bytes(&data_to_sign.clone())?; + tx_data_to_sign.put_bytes(&inputs.clone())?; + + let signature = keypair.sign(&tx_data_to_sign.into_inner()); + + let mut witness = Codec::new(Vec::new()); + witness.put_u8(WITNESS_SCHEME)?; + witness.put_be_u32(NONCE)?; + witness.put_bytes(signature.as_bytes())?; + let witnesses = witness.into_inner(); + + Ok((inputs, witnesses)) +} + +/// compose encrypted vote and proof in bytes +pub fn compose_encrypted_vote_part( + ciphertexts: Vec, + proof: ProofOfCorrectVote, +) -> Result<(Vec, Vec), Box> { + let mut encrypted_vote = Codec::new(Vec::new()); + + let size_element = ciphertexts.iter().len(); + for cipher in ciphertexts.iter() { + encrypted_vote.put_bytes(&cipher.to_bytes())?; + } + + let encrypted_bytes = encrypted_vote.into_inner(); + + // prepend with SIZE-ELEMENT-8BIT + let mut encrypted_vote = Codec::new(Vec::new()); + encrypted_vote.put_u8(size_element as u8)?; + encrypted_vote.put_bytes(encrypted_bytes.as_slice())?; + + let mut proof_bytes = Codec::new(Vec::new()); + + for announcement in proof.announcments_group_elements() { + proof_bytes.put_bytes(&announcement.to_bytes())?; + } + + for cipher in proof.ds() { + proof_bytes.put_bytes(&cipher.to_bytes())?; + } + + for response in proof.zwvs() { + proof_bytes.put_bytes(&response.to_bytes())?; + } + + proof_bytes.put_bytes(&proof.r().to_bytes())?; + + // prepend with SIZE-ELEMENT-8BIT + let mut proof_vote = Codec::new(Vec::new()); + proof_vote.put_u8(proof.len() as u8)?; + proof_vote.put_bytes(proof_bytes.into_inner().as_slice())?; + + let mut proof = Codec::new(Vec::new()); + + proof.put_bytes(&proof_vote.into_inner())?; + + Ok((proof.into_inner(), encrypted_vote.into_inner())) +} + +#[cfg(test)] +mod tests { + + use chain_addr::{AddressReadable, Discrimination}; + + use chain_impl_mockchain::{fragment::Fragment, transaction::InputEnum}; + use chain_ser::{deser::DeserializeFromSlice, packer::Codec}; + + use ed25519_dalek::Keypair; + use rand_core::OsRng; + + use chain_vote::{ + Ciphertext, Crs, ElectionPublicKey, MemberCommunicationKey, MemberState, ProofOfCorrectVote, + }; + + use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; + + use crate::fragment::{compose_encrypted_vote_part, generate_vote_fragment}; + use jormungandr_lib::interfaces::AccountIdentifier; + + #[test] + fn test_fragment_generation() { + let mut csprng = OsRng; + + // User key for signing witness + let keypair = Keypair::generate(&mut csprng); + + let pk = keypair.public.as_bytes().clone(); + + println!("Secret key: {}", hex::encode(keypair.secret.as_bytes())); + println!("Public key: {}", hex::encode(keypair.public.as_bytes())); + + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + + // vote plan id + let vote_plan_id = + "36ad42885189a0ac3438cdb57bc8ac7f6542e05a59d1f2e4d1d38194c9d4ac7b".to_owned(); + + // election public key + let ek = create_election_pub_key(vote_plan_id.clone(), rng.clone()); + + println!("election public key {:?}", hex::encode(ek.to_bytes())); + + // vote + let vote = chain_vote::Vote::new(2, 1 as usize).unwrap(); + + let crs = chain_vote::Crs::from_hash(vote_plan_id.as_bytes()); + + let (ciphertexts, proof) = ek.encrypt_and_prove_vote(&mut rng, &crs, vote); + let (proof, encrypted_vote) = + compose_encrypted_vote_part(ciphertexts.clone(), proof).unwrap(); + + // generate fragment + let fragment_bytes = generate_vote_fragment( + keypair, + encrypted_vote, + proof, + 5, + &hex::decode(vote_plan_id.clone()).unwrap(), + ) + .unwrap(); + + println!( + "generated fragment: {:?} size:{:?}", + hex::encode(fragment_bytes.clone()), + fragment_bytes.len() + ); + + let fragment = Fragment::deserialize_from_slice(&mut Codec::new(&fragment_bytes)).unwrap(); + + if let Fragment::VoteCast(tx) = fragment.clone() { + let _fragment_id = fragment.hash(); + + let input = tx.as_slice().inputs().iter().next().unwrap().to_enum(); + let caster = if let InputEnum::AccountInput(account_id, _value) = input { + AccountIdentifier::from(account_id).into_address(Discrimination::Production, "ca") + } else { + panic!("unhandled input "); + }; + let certificate = tx.as_slice().payload().into_payload(); + + let voting_key_61824_format = AddressReadable::from_string("ca", &caster.to_string()) + .unwrap() + .to_address(); + + let voting_key = voting_key_61824_format.public_key().unwrap().to_string(); + + assert_eq!(voting_key, hex::encode(pk)); + assert_eq!(certificate.proposal_index(), 5); + assert_eq!(certificate.vote_plan().to_string(), vote_plan_id); + } + } + + fn create_election_pub_key(shared_string: String, mut rng: ChaCha20Rng) -> ElectionPublicKey { + let h = Crs::from_hash(shared_string.as_bytes()); + let mc1 = MemberCommunicationKey::new(&mut rng); + let mc = [mc1.to_public()]; + let threshold = 1; + let m1 = MemberState::new(&mut rng, threshold, &h, &mc, 0); + let participants = vec![m1.public_key()]; + let ek = ElectionPublicKey::from_participants(&participants); + ek + } + + #[test] + fn generate_keys_from_bytes() { + let pk = hex::decode( + "ac247e6cbc2106a8858d67a9b6aa9fc6105a2f42abfd8d269f4096488b7e5d81".to_string(), + ) + .unwrap(); + + let mut sk = hex::decode( + "40cc7f02e04324b63a4db949854d5f24c9041a2bebe9b42064ff868071d1d72d".to_string(), + ) + .unwrap(); + + sk.extend(pk.clone()); + let keys = sk.clone(); + let keypair: Keypair = Keypair::from_bytes(&keys).unwrap(); + + assert_eq!(hex::encode(keypair.public.as_bytes()), hex::encode(pk)); + + println!("Secret key: {}", hex::encode(keypair.secret.as_bytes())); + println!("Public key: {}", hex::encode(keypair.public.as_bytes())); + } + + #[test] + fn test_encrypted_vote_generation() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + + // vote plan id + let vote_plan_id = + "36ad42885189a0ac3438cdb57bc8ac7f6542e05a59d1f2e4d1d38194c9d4ac7b".to_owned(); + + let shared_string = vote_plan_id.to_owned(); + + // election public key + let ek = create_election_pub_key(shared_string, rng.clone()); + + let vote = chain_vote::Vote::new(2, 1 as usize).unwrap(); + let crs = chain_vote::Crs::from_hash(vote_plan_id.as_bytes()); + + let (ciphertexts, proof) = ek.encrypt_and_prove_vote(&mut rng, &crs, vote); + let (proof, mut enc_vote) = + compose_encrypted_vote_part(ciphertexts.clone(), proof).unwrap(); + + // remove size element, size element is 2 meaning there two ciphertexts + enc_vote.remove(0); + // each ciphertext consists of two 32 byte group elements + let (cipher_a, cipher_b) = enc_vote.split_at(64); + + let _cipher_a = Ciphertext::from_bytes(cipher_a).unwrap(); + let _cipher_b = Ciphertext::from_bytes(cipher_b).unwrap(); + + let mut msg = Codec::new(proof.as_slice()); + + let p = ProofOfCorrectVote::from_buffer(&mut msg).unwrap(); + + assert_eq!(p.len(), 1); + } +} diff --git a/src/sign/src/main.rs b/src/sign/src/main.rs new file mode 100644 index 0000000000..781e495f6e --- /dev/null +++ b/src/sign/src/main.rs @@ -0,0 +1,86 @@ +//! +//! Fragment generator +//! + +use bech32::Error as Bech32Error; +use bech32::FromBase32; +use chain_vote::ElectionPublicKey; +use clap::Parser; +use color_eyre::Result; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; + +use ed25519_dalek::*; +use std::error::Error; + +use crate::fragment::{compose_encrypted_vote_part, generate_vote_fragment}; + +mod fragment; + +/// +/// Args defines and declares CLI behaviour within the context of clap +/// +#[derive(Parser, Debug, Clone)] +#[clap(about, version, author)] +pub struct Args { + /// Election public key issued by Trent + #[clap(short, long)] + pub election_pub_key: String, + /// Public key of Alice + #[clap(short, long)] + public_key: String, + /// Private key of Alice + #[clap(short, long)] + private_key: String, + /// proposal to vote on + #[clap(short, long)] + proposal: u8, + /// vote plan hash + #[clap(short, long)] + vote_plan_id: String, +} + +fn main() -> Result<(), Box> { + color_eyre::install()?; + + let args = Args::parse(); + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + + let pk = hex::decode(args.public_key)?; + let mut sk = hex::decode(args.private_key)?; + + // Election pub key published as a Bech32_encoded address + // which consists of 3 parts: A Human-Readable Part (HRP) + Separator + Data: + let (_hrp, data, _variant) = + bech32::decode(&args.election_pub_key).map_err(Bech32Error::from)?; + + let election_pk = Vec::::from_base32(&data).map_err(Bech32Error::from)?; + + // join sk+pk together, api requirement + sk.extend(pk.clone()); + let keypair: Keypair = Keypair::from_bytes(&sk)?; + + // vote + let vote = chain_vote::Vote::new(2, 1_usize)?; + let crs = chain_vote::Crs::from_hash(args.vote_plan_id.clone().as_bytes()); + + // parse ek key + let ek = ElectionPublicKey::from_bytes(&election_pk) + .ok_or("unable to parse election pub key".to_string())?; + + let (ciphertexts, proof) = ek.encrypt_and_prove_vote(&mut rng, &crs, vote); + let (proof, encrypted_vote) = compose_encrypted_vote_part(ciphertexts.clone(), proof)?; + + let fragment_bytes = generate_vote_fragment( + keypair, + encrypted_vote, + proof, + args.proposal, + args.vote_plan_id.as_bytes(), + )?; + + // fragment in hex: output consumed as input to another program + println!("{:?}", hex::encode(fragment_bytes.clone())); + + Ok(()) +}