From 47ec5eb6c618a5b97b83a34341c7e0d1f241a9f7 Mon Sep 17 00:00:00 2001 From: Markus Triska Date: Tue, 26 Dec 2023 07:24:18 +0100 Subject: [PATCH] ENHANCED: Use crrl for Ed25519 signing and signature verification. The main motivation for this change is the introduction of the newly available predicate ed25519_seed_keypair/2, allowing to generate a key pair from a given seed. In this way, a key pair can be dynamically generated from (for example) a password, using crypto_password_hash/3 in combination with crypto_data_hkdf/4 to generate the seed. The advantage of this method is that the private key need not be stored at all anywhere. It is not possible to add a corresponding feature to ring, since it is closed as "not planned": https://github.com/briansmith/ring/issues/1003 I also used this opportunity to move more of the logic to Prolog. We now have total control of the key pair representation, and I also changed the representation to conform to the PKCS#8 v2 standard, something that only later ring versions do, while still being backwards compatible with tools that produce a wrong representation including earlier ring versions. Another great advantage we get from this change is that the Ed25519 predicates now also run on the 32-bit and WASM versions of Scryer. --- build/instructions_template.rs | 40 +++++++---------- src/lib/crypto.pl | 53 +++++++++++++++++++---- src/machine/dispatch.rs | 40 ++++++----------- src/machine/system_calls.rs | 79 +++++++++++----------------------- 4 files changed, 99 insertions(+), 113 deletions(-) diff --git a/build/instructions_template.rs b/build/instructions_template.rs index db0baae3..fee8e28f 100644 --- a/build/instructions_template.rs +++ b/build/instructions_template.rs @@ -511,18 +511,12 @@ enum SystemClauseType { #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "6", Name = "$crypto_data_decrypt")))] CryptoDataDecrypt, - #[cfg(feature = "crypto-full")] - #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_sign")))] - Ed25519Sign, - #[cfg(feature = "crypto-full")] - #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_verify")))] - Ed25519Verify, - #[cfg(feature = "crypto-full")] - #[strum_discriminants(strum(props(Arity = "1", Name = "$ed25519_new_keypair")))] - Ed25519NewKeyPair, - #[cfg(feature = "crypto-full")] - #[strum_discriminants(strum(props(Arity = "2", Name = "$ed25519_keypair_public_key")))] - Ed25519KeyPairPublicKey, + #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_sign_raw")))] + Ed25519SignRaw, + #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_verify_raw")))] + Ed25519VerifyRaw, + #[strum_discriminants(strum(props(Arity = "2", Name = "$ed25519_seed_to_public_key")))] + Ed25519SeedToPublicKey, #[strum_discriminants(strum(props(Arity = "2", Name = "$first_non_octet")))] FirstNonOctet, #[strum_discriminants(strum(props(Arity = "3", Name = "$load_html")))] @@ -1874,18 +1868,17 @@ fn generate_instruction_preface() -> TokenStream { &Instruction::CallFlushTermQueue | &Instruction::CallRemoveModuleExports | &Instruction::CallAddNonCountedBacktracking | - &Instruction::CallPopCount => { + &Instruction::CallPopCount | + &Instruction::CallEd25519SignRaw | + &Instruction::CallEd25519VerifyRaw | + &Instruction::CallEd25519SeedToPublicKey => { let (name, arity) = self.to_name_and_arity(); functor!(atom!("call"), [atom(name), fixnum(arity)]) } // #[cfg(feature = "crypto-full")] &Instruction::CallCryptoDataEncrypt | - &Instruction::CallCryptoDataDecrypt | - &Instruction::CallEd25519Sign | - &Instruction::CallEd25519Verify | - &Instruction::CallEd25519NewKeyPair | - &Instruction::CallEd25519KeyPairPublicKey => { + &Instruction::CallCryptoDataDecrypt => { let (name, arity) = self.to_name_and_arity(); functor!(atom!("call"), [atom(name), fixnum(arity)]) } @@ -2110,18 +2103,17 @@ fn generate_instruction_preface() -> TokenStream { &Instruction::ExecuteFlushTermQueue | &Instruction::ExecuteRemoveModuleExports | &Instruction::ExecuteAddNonCountedBacktracking | - &Instruction::ExecutePopCount => { + &Instruction::ExecutePopCount | + &Instruction::ExecuteEd25519SignRaw | + &Instruction::ExecuteEd25519VerifyRaw | + &Instruction::ExecuteEd25519SeedToPublicKey => { let (name, arity) = self.to_name_and_arity(); functor!(atom!("execute"), [atom(name), fixnum(arity)]) } // #[cfg(feature = "crypto-full")] &Instruction::ExecuteCryptoDataEncrypt | - &Instruction::ExecuteCryptoDataDecrypt | - &Instruction::ExecuteEd25519Sign | - &Instruction::ExecuteEd25519Verify | - &Instruction::ExecuteEd25519NewKeyPair | - &Instruction::ExecuteEd25519KeyPairPublicKey => { + &Instruction::ExecuteCryptoDataDecrypt => { let (name, arity) = self.to_name_and_arity(); functor!(atom!("execute"), [atom(name), fixnum(arity)]) } diff --git a/src/lib/crypto.pl b/src/lib/crypto.pl index bec12d51..b03c544a 100644 --- a/src/lib/crypto.pl +++ b/src/lib/crypto.pl @@ -25,6 +25,7 @@ crypto_password_hash/3, % +Password, -Hash, +Options crypto_data_encrypt/6, % +PlainText, +Algorithm, +Key, +IV, -CipherText, +Options crypto_data_decrypt/6, % +CipherText, +Algorithm, +Key, +IV, -PlainText, +Options + ed25519_seed_keypair/2, % +Seed, -KeyPair ed25519_new_keypair/1, % -KeyPair ed25519_keypair_public_key/2, % +KeyPair, +PublicKey ed25519_sign/4, % +KeyPair, +Data, -Signature, +Options @@ -612,6 +613,34 @@ encoding_chars(utf8, Cs, Cs) :- =============================== - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +%% ed25519_seed_keypair(+Seed, -Pair) +% +% Use Seed to deterministically generate an Ed25519 key pair Pair, a +% list of characters. Seed must be a list of 32 bytes. It can be +% chosen at random (using for example `crypto_n_random_bytes/2`) or +% derived from input keying material (IKM) using for example +% `crypto_data_hkdf/4`. The pair contains the private key and must be +% kept absolutely secret. Pair can be used for signing. Its public +% key can be obtained with `ed25519_keypair_public_key/2`. + +ed25519_seed_keypair(Seed, Pair) :- + must_be_bytes(Seed, ed25519_keypair_from_seed/2), + length(Seed, 32), + '$ed25519_seed_to_public_key'(Seed, Public), + maplist(char_code, Public, PublicBytes), + phrase(([0x30,81], % a sequence of 81 bytes follows + [2,1], % the integer 1 denoting version 2 (awesome design!) + [1], % the public key is also present + [48,5], % an element of 5 bytes follows + [6,3,43,101,112], % OID of Ed25519 + [4,34], % an octet string of 34 bytes follows + [4,32], % an octet string of 32 bytes follows + seq(Seed), % the seed is the private key + [129,33], + [0], % 32 bytes is divisible by 8 + seq(PublicBytes)), ASN1), + maplist(char_code, Pair, ASN1). + %% ed25519_new_keypair(-Pair) % % Yields a new Ed25519 key pair Pair, a list of characters. The @@ -620,7 +649,8 @@ encoding_chars(utf8, Cs, Cs) :- % with `ed25519_keypair_public_key/2`. ed25519_new_keypair(Pair) :- - '$ed25519_new_keypair'(Pair). + crypto_n_random_bytes(32, Bytes), + ed25519_seed_keypair(Bytes, Pair). %% ed25519_keypair_public_key(+Pair, -PublicKey) % @@ -629,8 +659,11 @@ ed25519_new_keypair(Pair) :- % The public key is represented as a list of characters. ed25519_keypair_public_key(Pair, PublicKey) :- - must_be_octet_chars(Pair, ed25519_keypair_public_key), - '$ed25519_keypair_public_key'(Pair, PublicKey). + must_be_octet_chars(Pair, ed25519_keypair_public_key/2), + reverse(Pair, RPs), + length(RPublicKey, 32), + phrase((seq(RPublicKey),...), RPs), + reverse(RPublicKey, PublicKey). %% ed25519_sign(+Key, +Data, -Signature, +Options) % @@ -638,10 +671,14 @@ ed25519_keypair_public_key(Pair, PublicKey) :- % PKCS#8 v2 format as generated by `ed25519_new_keypair/1`. Sign Data % with Key, yielding Signature as a list of hexadecimal characters. -ed25519_sign(Key, Data0, Signature, Options) :- - must_be_octet_chars(Key, ed25519_sign), +ed25519_sign(KeyPair, Data0, Signature, Options) :- + must_be_octet_chars(KeyPair, ed25519_sign/4), + length(Prefix, 16), + length(PrivateKeyChars, 32), + phrase((seq(Prefix),seq(PrivateKeyChars),...), KeyPair), + maplist(char_code, PrivateKeyChars, PrivateKey), options_data_chars(Options, Data0, Data, Encoding), - '$ed25519_sign'(Key, Data, Encoding, Signature0), + '$ed25519_sign_raw'(PrivateKey, Data, Encoding, Signature0), hex_bytes(Signature, Signature0). %% ed25519_verify(+Key, +Data, +Signature, +Options) @@ -658,10 +695,10 @@ ed25519_sign(Key, Data0, Signature, Options) :- % which treats Data as a list of raw bytes. ed25519_verify(Key, Data0, Signature0, Options) :- - must_be_octet_chars(Key, ed25519_verify), + must_be_octet_chars(Key, ed25519_verify/4), options_data_chars(Options, Data0, Data, Encoding), hex_bytes(Signature0, Signature), - '$ed25519_verify'(Key, Data, Encoding, Signature). + '$ed25519_verify_raw'(Key, Data, Encoding, Signature). /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - X25519: ECDH key exchange over Curve25519 diff --git a/src/machine/dispatch.rs b/src/machine/dispatch.rs index ff7d0188..29f05401 100644 --- a/src/machine/dispatch.rs +++ b/src/machine/dispatch.rs @@ -4491,44 +4491,28 @@ impl Machine { self.crypto_curve_scalar_mult(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } - #[cfg(feature = "crypto-full")] - &Instruction::CallEd25519Sign => { - self.ed25519_sign(); - step_or_fail!(self, self.machine_st.p += 1); - } - #[cfg(feature = "crypto-full")] - &Instruction::ExecuteEd25519Sign => { - self.ed25519_sign(); - step_or_fail!(self, self.machine_st.p = self.machine_st.cp); - } - #[cfg(feature = "crypto-full")] - &Instruction::CallEd25519Verify => { - self.ed25519_verify(); + &Instruction::CallEd25519SignRaw => { + self.ed25519_sign_raw(); step_or_fail!(self, self.machine_st.p += 1); } - #[cfg(feature = "crypto-full")] - &Instruction::ExecuteEd25519Verify => { - self.ed25519_verify(); + &Instruction::ExecuteEd25519SignRaw => { + self.ed25519_sign_raw(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } - #[cfg(feature = "crypto-full")] - &Instruction::CallEd25519NewKeyPair => { - self.ed25519_new_key_pair(); + &Instruction::CallEd25519VerifyRaw => { + self.ed25519_verify_raw(); step_or_fail!(self, self.machine_st.p += 1); } - #[cfg(feature = "crypto-full")] - &Instruction::ExecuteEd25519NewKeyPair => { - self.ed25519_new_key_pair(); + &Instruction::ExecuteEd25519VerifyRaw => { + self.ed25519_verify_raw(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } - #[cfg(feature = "crypto-full")] - &Instruction::CallEd25519KeyPairPublicKey => { - self.ed25519_key_pair_public_key(); + &Instruction::CallEd25519SeedToPublicKey => { + self.ed25519_seed_to_public_key(); step_or_fail!(self, self.machine_st.p += 1); } - #[cfg(feature = "crypto-full")] - &Instruction::ExecuteEd25519KeyPairPublicKey => { - self.ed25519_key_pair_public_key(); + &Instruction::ExecuteEd25519SeedToPublicKey => { + self.ed25519_seed_to_public_key(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } &Instruction::CallCurve25519ScalarMult => { diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 2e3c1d54..c5beaf9a 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -81,14 +81,11 @@ use ring::rand::{SecureRandom, SystemRandom}; use ring::{digest, hkdf, pbkdf2}; #[cfg(feature = "crypto-full")] -use ring::{ - aead, - signature::{self, KeyPair}, -}; +use ring::aead; use ripemd160::{Digest, Ripemd160}; use sha3::{Sha3_224, Sha3_256, Sha3_384, Sha3_512}; -use crrl::{secp256k1, x25519}; +use crrl::{ed25519, secp256k1, x25519}; #[cfg(feature = "tls")] use native_tls::{Identity, TlsAcceptor, TlsConnector}; @@ -7617,33 +7614,16 @@ impl Machine { unify!(self.machine_st, self.machine_st.registers[4], uncompressed); } - #[cfg(feature = "crypto-full")] #[inline(always)] - pub(crate) fn ed25519_new_key_pair(&mut self) { - let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(rng()).unwrap(); - let complete_string = self.u8s_to_string(pkcs8_bytes.as_ref()); - - unify!( - self.machine_st, - self.machine_st.registers[1], - complete_string - ) - } - - #[cfg(feature = "crypto-full")] - #[inline(always)] - pub(crate) fn ed25519_key_pair_public_key(&mut self) { - let bytes = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); + pub(crate) fn ed25519_seed_to_public_key(&mut self) { + let stub_gen = || functor_stub(atom!("ed25519_seed_keypair"), 2); + let seed_bytes = self + .machine_st + .integers_to_bytevec(self.machine_st.registers[1], stub_gen); - let key_pair = match signature::Ed25519KeyPair::from_pkcs8(&bytes) { - Ok(kp) => kp, - _ => { - self.machine_st.fail = true; - return; - } - }; + let skey = ed25519::PrivateKey::from_seed(&seed_bytes); - let complete_string = self.u8s_to_string(key_pair.public_key().as_ref()); + let complete_string = self.u8s_to_string(skey.public_key.encoded.as_ref()); unify!( self.machine_st, @@ -7652,22 +7632,19 @@ impl Machine { ); } - #[cfg(feature = "crypto-full")] #[inline(always)] - pub(crate) fn ed25519_sign(&mut self) { - let key = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); + pub(crate) fn ed25519_sign_raw(&mut self) { + let stub_gen = || functor_stub(atom!("ed25519_sign"), 4); + let seed_bytes = self + .machine_st + .integers_to_bytevec(self.machine_st.registers[1], stub_gen); + + let skey = ed25519::PrivateKey::from_seed(&seed_bytes); + let encoding = cell_as_atom!(self.deref_register(3)); let data = self.string_encoding_bytes(self.machine_st.registers[2], encoding); - let key_pair = match signature::Ed25519KeyPair::from_pkcs8(&key) { - Ok(kp) => kp, - _ => { - self.machine_st.fail = true; - return; - } - }; - - let sig = key_pair.sign(&data); + let sig = skey.sign_raw(&data); let sig_list = heap_loc_as_cell!(iter_to_heap_list( &mut self.machine_st.heap, @@ -7679,25 +7656,21 @@ impl Machine { unify!(self.machine_st, self.machine_st.registers[4], sig_list); } - #[cfg(feature = "crypto-full")] #[inline(always)] - pub(crate) fn ed25519_verify(&mut self) { - let key = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); + pub(crate) fn ed25519_verify_raw(&mut self) { + let key_bytes = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); + let pkey = ed25519::PublicKey::decode(&key_bytes).unwrap(); + let encoding = cell_as_atom!(self.deref_register(3)); let data = self.string_encoding_bytes(self.machine_st.registers[2], encoding); - let stub_gen = || functor_stub(atom!("ed25519_verify"), 5); + + let stub_gen = || functor_stub(atom!("ed25519_verify"), 4); + let signature = self .machine_st .integers_to_bytevec(self.machine_st.registers[4], stub_gen); - let peer_public_key = signature::UnparsedPublicKey::new(&signature::ED25519, &key); - - match peer_public_key.verify(&data, &signature) { - Ok(_) => {} - _ => { - self.machine_st.fail = true; - } - } + self.machine_st.fail = !pkey.verify_raw(&signature, &data); } #[inline(always)] -- 2.54.0