Skip to content

Commit

Permalink
runtime-sdk: add bw compat to ed25519 signer
Browse files Browse the repository at this point in the history
  • Loading branch information
nhynes committed Oct 4, 2023
1 parent a76a420 commit a700acf
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 20 deletions.
2 changes: 1 addition & 1 deletion runtime-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
169 changes: 150 additions & 19 deletions runtime-sdk/src/crypto/signature/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -118,54 +118,185 @@ impl From<PublicKey> for CorePublicKey {

/// A memory-backed signer for Ed25519.
pub struct MemorySigner {
sk: ed25519_dalek::SigningKey,
key: Key,
}

/// The original version of the Ed25519 signer returned the "expanded" secret key from `to_bytes`.
/// A contract may have stored the "expanded" key and expects its use to continue to succeed.
/// For backwards compatibility, the signer works with both "expanded" and regular keys.
/// New invocations receive a regular/proper key, and from-"expanded" ones get the old behavior.
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 it is not recoverable from `esk`.
hash: zeroize::Zeroizing<[u8; 64]>,
},
Regular(ed25519_dalek::SigningKey),
}

impl Key {
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
match bytes.len() {
// It's a new/correct-style secret key.
32 => bytes
.try_into()
.map(ed25519_dalek::SigningKey::from_bytes)
.map(Self::Regular)
.map_err(|_| Error::MalformedPrivateKey),

Check warning on line 146 in runtime-sdk/src/crypto/signature/ed25519.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/crypto/signature/ed25519.rs#L146

Added line #L146 was not covered by tests
// It's an "expanded" secret key, which is treated as the output of a 64-byte hash function.
64 => bytes
.try_into()
.map(|hash| Self::Expanded {
esk: ed25519_dalek::hazmat::ExpandedSecretKey::from_bytes(&hash),
hash: hash.into(),
})
.map_err(|_| Error::MalformedPrivateKey),
_ => Err(Error::MalformedPrivateKey),

Check warning on line 155 in runtime-sdk/src/crypto/signature/ed25519.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/crypto/signature/ed25519.rs#L154-L155

Added lines #L154 - L155 were not covered by tests
}
}

fn to_bytes(&self) -> Vec<u8> {
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::<Sha512>(esk, message, &verifying_key)
}
Self::Regular(sk) => sk.sign(message),
}
.to_bytes()
.to_vec()
.into()
}

fn sign_digest<D>(&self, digest: D) -> Result<Signature, Error>
where
D: ed25519_dalek::Digest<OutputSize = U64>,
{
match self {
Key::Expanded { esk, .. } => {
let verifying_key = ed25519_dalek::VerifyingKey::from(esk);
ed25519_dalek::hazmat::raw_sign_prehashed::<Sha512, _>(
esk,

Check warning on line 187 in runtime-sdk/src/crypto/signature/ed25519.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/crypto/signature/ed25519.rs#L187

Added line #L187 was not covered by tests
digest,
&verifying_key,

Check warning on line 189 in runtime-sdk/src/crypto/signature/ed25519.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/crypto/signature/ed25519.rs#L189

Added line #L189 was not covered by tests
None,
)
}
Key::Regular(sk) => sk.sign_prehashed(digest, None),
}
.map_err(|_| Error::SigningError)

Check warning on line 195 in runtime-sdk/src/crypto/signature/ed25519.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/crypto/signature/ed25519.rs#L195

Added line #L195 was not covered by tests
.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 {
pub fn sign_digest<D>(&self, digest: D) -> Result<Signature, Error>
where
D: ed25519_dalek::Digest<OutputSize = U64>,
{
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<Self, Error> {
if seed.len() != 32 {
return Err(Error::MalformedPublicKey);

Check warning on line 220 in runtime-sdk/src/crypto/signature/ed25519.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/crypto/signature/ed25519.rs#L220

Added line #L220 was not covered by tests
}
Self::from_bytes(seed)
}

fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
Ok(Self {
sk: bytes.try_into().map_err(|_| Error::MalformedPrivateKey)?,
key: Key::from_bytes(bytes)?,
})
}

fn to_bytes(&self) -> Vec<u8> {
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<Signature, Error> {
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<Signature, Error> {
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"
);
}
}

0 comments on commit a700acf

Please sign in to comment.