From aa4c87bfa093849178c70dff9dbc8e2371bab943 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Tue, 3 Oct 2023 19:39:00 +0000 Subject: [PATCH] runtime-sdk: add bw compat to ed25519 signer --- runtime-sdk/Cargo.toml | 2 +- runtime-sdk/src/crypto/signature/ed25519.rs | 164 +++++++++++++++++--- 2 files changed, 146 insertions(+), 20 deletions(-) diff --git a/runtime-sdk/Cargo.toml b/runtime-sdk/Cargo.toml index bcfa841dbd..864edf7f23 100644 --- a/runtime-sdk/Cargo.toml +++ b/runtime-sdk/Cargo.toml @@ -14,7 +14,7 @@ oasis-runtime-sdk-macros = { path = "../runtime-sdk-macros", optional = true } # Third party. byteorder = "1.4.3" curve25519-dalek = "3.2.0" -ed25519-dalek = { version = "2.0.0", features = ["digest"] } +ed25519-dalek = { version = "2.0.0", features = ["digest", "hazmat"] } digest = "0.10.3" hmac = "0.12.1" sha2 = "0.10.8" diff --git a/runtime-sdk/src/crypto/signature/ed25519.rs b/runtime-sdk/src/crypto/signature/ed25519.rs index 3bfb50e5f4..13904301ee 100644 --- a/runtime-sdk/src/crypto/signature/ed25519.rs +++ b/runtime-sdk/src/crypto/signature/ed25519.rs @@ -3,13 +3,13 @@ use std::convert::TryInto; use curve25519_dalek::{digest::consts::U64, edwards::CompressedEdwardsY}; use ed25519_dalek::Signer as _; -use sha2::{Digest as _, Sha512_256}; +use sha2::{Digest as _, Sha512, Sha512_256}; use oasis_core_runtime::common::crypto::signature::{ PublicKey as CorePublicKey, Signature as CoreSignature, }; -use crate::crypto::signature::{Error, Signature}; +use crate::crypto::signature::{Error, Signature, Signer}; /// An Ed25519 public key. #[derive(Clone, Debug, PartialEq, Eq, cbor::Encode, cbor::Decode)] @@ -118,7 +118,86 @@ impl From for CorePublicKey { /// A memory-backed signer for Ed25519. pub struct MemorySigner { - sk: ed25519_dalek::SigningKey, + key: Key, +} + +/// The original version of this signer returned the "expanded" signing key from `to_bytes`. +/// If the contract passes in one of these, it's gets the same thing out. New invocations get the "proper" version. +enum Key { + Expanded { + esk: ed25519_dalek::hazmat::ExpandedSecretKey, + /// The hash output that is used to create the "expanded" secret key. It is stored to return from `from_bytes` because `esk` is a function of the hash, not the hash directly. + hash: [u8; 64], + }, + Regular(ed25519_dalek::SigningKey), +} + +impl Key { + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() == 64 { + let hash: [u8; 64] = bytes.try_into().unwrap(); + Ok(Self::Expanded { + esk: ed25519_dalek::hazmat::ExpandedSecretKey::from_bytes(&hash), + hash, + }) + } else if bytes.len() == 32 { + bytes + .try_into() + .map(ed25519_dalek::SigningKey::from_bytes) + .map(Self::Regular) + .map_err(|_| Error::MalformedPrivateKey) + } else { + Err(Error::MalformedPrivateKey) + } + } + + fn to_bytes(&self) -> Vec { + match self { + Self::Expanded { hash, .. } => hash.to_vec(), + Self::Regular(sk) => sk.to_bytes().to_vec(), + } + } + + fn sign(&self, message: &[u8]) -> Signature { + match self { + Self::Expanded { esk, .. } => { + let verifying_key = ed25519_dalek::VerifyingKey::from(esk); + ed25519_dalek::hazmat::raw_sign::(esk, message, &verifying_key) + } + Self::Regular(sk) => sk.sign(message), + } + .to_bytes() + .to_vec() + .into() + } + + fn sign_digest(&self, digest: D) -> Result + where + D: ed25519_dalek::Digest, + { + match self { + Key::Expanded { esk, .. } => { + let verifying_key = ed25519_dalek::VerifyingKey::from(esk); + ed25519_dalek::hazmat::raw_sign_prehashed::( + esk, + digest, + &verifying_key, + None, + ) + } + Key::Regular(sk) => sk.sign_prehashed(digest, None), + } + .map_err(|_| Error::SigningError) + .map(|sig| sig.to_bytes().to_vec().into()) + } + + fn public_key(&self) -> super::PublicKey { + let pk = match self { + Self::Expanded { esk, .. } => ed25519_dalek::VerifyingKey::from(esk), + Self::Regular(sk) => sk.verifying_key(), + }; + super::PublicKey::Ed25519(PublicKey::from_bytes(pk.as_bytes()).unwrap()) + } } impl MemorySigner { @@ -126,46 +205,93 @@ impl MemorySigner { where D: ed25519_dalek::Digest, { - self.sk - .sign_prehashed(digest, None) - .map_err(|_| Error::SigningError) - .map(|sig| sig.to_bytes().to_vec().into()) + self.key.sign_digest(digest) } } -impl super::Signer for MemorySigner { +impl Signer for MemorySigner { fn new_from_seed(seed: &[u8]) -> Result { + if seed.len() != 32 { + return Err(Error::MalformedPublicKey); + } Self::from_bytes(seed) } fn from_bytes(bytes: &[u8]) -> Result { Ok(Self { - sk: bytes.try_into().map_err(|_| Error::MalformedPrivateKey)?, + key: Key::from_bytes(bytes)?, }) } fn to_bytes(&self) -> Vec { - self.sk.to_bytes().to_vec() + self.key.to_bytes() } fn public_key(&self) -> super::PublicKey { - let pk = ed25519_dalek::VerifyingKey::from(&self.sk); - super::PublicKey::Ed25519(PublicKey::from_bytes(pk.as_bytes()).unwrap()) + self.key.public_key() } fn sign(&self, context: &[u8], message: &[u8]) -> Result { let mut digest = Sha512_256::new(); digest.update(context); digest.update(message); - let message = digest.finalize(); - - let signature = self.sk.sign(&message); - - Ok(signature.to_bytes().to_vec().into()) + Ok(self.key.sign(&digest.finalize())) } fn sign_raw(&self, message: &[u8]) -> Result { - let signature = self.sk.sign(message); - Ok(signature.to_bytes().to_vec().into()) + Ok(self.key.sign(message)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn legacy_esk_equivalence() { + let seed = [42u8; 32]; + let signer = MemorySigner::new_from_seed(&seed).unwrap(); + + let esk = ed25519_dalek::hazmat::ExpandedSecretKey::from(&seed); + let esk_hash = Sha512::digest(seed); + let esk_signer = MemorySigner::from_bytes(&esk_hash).unwrap(); + + let esk_public_key = super::super::PublicKey::Ed25519( + PublicKey::from_bytes(&ed25519_dalek::VerifyingKey::from(&esk).to_bytes()).unwrap(), + ); + + assert_eq!( + esk_signer.to_bytes().as_slice(), + esk_hash.as_slice(), + "esk roundtrip" + ); + assert_eq!(signer.to_bytes(), seed, "sk roundtrip"); + + let context = b"tests"; + let message = b"hello, world!"; + let digest = Sha512::new().chain_update(context).chain_update(message); + + let sig = signer.sign(context, message).unwrap(); + let esk_sig = esk_signer.sign(context, message).unwrap(); + assert_eq!(sig, esk_sig, "sig != esk_sig"); + + let raw_sig = signer.sign_raw(message).unwrap(); + let esk_raw_sig = esk_signer.sign_raw(message).unwrap(); + assert_eq!(raw_sig, esk_raw_sig, "raw_sig != esk_raw_sig"); + + let digest_sig = signer.sign_digest(digest.clone()).unwrap(); + let esk_digest_sig = esk_signer.sign_digest(digest).unwrap(); + assert_eq!(digest_sig, esk_digest_sig, "digest_sig != esk_digest_sig"); + + assert_eq!( + signer.public_key(), + esk_public_key, + "signer pk != esk_public_key" + ); + assert_eq!( + esk_signer.public_key(), + esk_public_key, + "esk_signer pk != esk_pubic_key" + ); } }