diff --git a/Cargo.lock b/Cargo.lock index 6334fcd..74bc850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3146,6 +3146,7 @@ dependencies = [ "eyre", "hex-literal", "itertools 0.13.0", + "rand_core 0.6.4", "thiserror", ] @@ -3299,8 +3300,10 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2 0.10.8", + "signature", "subtle", "zeroize", ] @@ -7051,6 +7054,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] diff --git a/crates/did-simple/Cargo.toml b/crates/did-simple/Cargo.toml index 9dbd0fc..62bf4a1 100644 --- a/crates/did-simple/Cargo.toml +++ b/crates/did-simple/Cargo.toml @@ -9,11 +9,12 @@ description = "Dead simple DIDs" publish = true [features] -default = ["ed25519"] +default = ["ed25519", "random"] ed25519 = [ "dep:curve25519-dalek", "dep:ed25519-dalek", ] +random = ["dep:rand_core", "ed25519-dalek?/rand_core"] # Only applications should enable this! If you use did-simple as a dependency, # don't enable this feature - let applications set it instead. @@ -24,8 +25,9 @@ allow-unsafe = [] bs58 = "0.5.1" bytes = "1.6.0" thiserror = "1.0.60" -ed25519-dalek = { version = "2.1.1", optional = true } +ed25519-dalek = { version = "2.1.1", optional = true, features = ["digest"] } curve25519-dalek = { version = "4.1.2", optional = true } +rand_core = { version = "0.6.4", optional = true, features = ["getrandom"] } [dev-dependencies] eyre = "0.6.12" diff --git a/crates/did-simple/src/crypto/ed25519.rs b/crates/did-simple/src/crypto/ed25519.rs index 3679f73..7353539 100644 --- a/crates/did-simple/src/crypto/ed25519.rs +++ b/crates/did-simple/src/crypto/ed25519.rs @@ -1,25 +1,38 @@ +//! Implementation of the ed25519ph signing algorithm. + use curve25519_dalek::edwards::CompressedEdwardsY; -use ed25519_dalek::VerifyingKey; -use crate::key_algos::StaticKeyAlgo as _; +use super::Context; +use crate::key_algos::StaticSigningAlgo as _; + +pub use ed25519_dalek::{ed25519::Signature, Digest, Sha512, SignatureError}; -/// An ed25519 public key. -#[allow(dead_code)] -pub struct PubKey(VerifyingKey); +/// Re-exported for lower level control +pub use ed25519_dalek; -impl PubKey { +/// An ed25519 public key. Used for verifying messages. +/// +/// We recommend deserializing bytes into this type using +/// [`Self::try_from_bytes()`]. Then you can either use this type, which has +/// simpler function signatures, or you can call [`Self::into_inner()`] and use +/// the lower level [`ed25519_dalek`] crate directly, which is slightly less +/// opinionated and has more customization of options made available. +#[derive(Debug, Eq, PartialEq, Hash)] +pub struct VerifyingKey(ed25519_dalek::VerifyingKey); + +impl VerifyingKey { pub const LEN: usize = Self::key_len(); /// Instantiates `PubKey` from some bytes. Performs all necessary validation /// that the key is valid and of sufficient strength. /// /// Note that we will reject any keys that are too weak (aka low order). - pub fn try_from(bytes: &[u8; Self::LEN]) -> Result { + pub fn try_from_bytes(bytes: &[u8; Self::LEN]) -> Result { let compressed_edwards = CompressedEdwardsY(bytes.to_owned()); let Some(edwards) = compressed_edwards.decompress() else { return Err(TryFromBytesError::NotOnCurve); }; - let key = VerifyingKey::from(edwards); + let key = ed25519_dalek::VerifyingKey::from(edwards); if key.is_weak() { return Err(TryFromBytesError::WeakKey); } @@ -28,10 +41,174 @@ impl PubKey { // TODO: Turn this into inline const when that feature stabilizes const fn key_len() -> usize { - let len = crate::key_algos::Ed25519::PUB_KEY_LEN; + let len = crate::key_algos::Ed25519::VERIFYING_KEY_LEN; assert!(len == ed25519_dalek::PUBLIC_KEY_LENGTH); len } + + pub fn into_inner(self) -> ed25519_dalek::VerifyingKey { + self.0 + } + + /// Verifies `message` using the ed25519ph algorithm. + /// + /// # Example + /// ``` + /// use did_simple::crypto::{Context, ed25519::{SigningKey, VerifyingKey}}; + /// + /// let signing_key = SigningKey::random(); + /// let verifying_key = signing_key.verifying_key(); + /// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes()); + /// + /// let msg = "everyone can read and verify this message"; + /// let sig = signing_key.sign(msg, CTX); + /// + /// assert!(verifying_key.verify(msg, CTX, &sig).is_ok()); + /// ``` + pub fn verify( + &self, + message: impl AsRef<[u8]>, + context: Context, + signature: &Signature, + ) -> Result<(), SignatureError> { + let digest = Sha512::new().chain_update(message); + self.verify_digest(digest, context, signature) + } + + /// Same as `verify`, but allows you to populate `message_digest` separately + /// from signing. + /// + /// This can be useful if for example, it is undesirable to buffer the message + /// into a single slice, or the message is being streamed asynchronously. + /// You can instead update the digest chunk by chunk, and pass the digest + /// in after you are done reading all the data. + /// + /// # Example + /// + /// ``` + /// use did_simple::crypto::{Context, ed25519::{Sha512, Digest, SigningKey, VerifyingKey}}; + /// + /// let signing_key = SigningKey::random(); + /// let verifying_key = signing_key.verifying_key(); + /// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes()); + /// + /// let sig = signing_key.sign("this is my message", CTX); + /// let mut digest = Sha512::new(); + /// digest.update("this is "); + /// digest.update("my message"); + /// assert!(verifying_key.verify_digest(digest, CTX, &sig).is_ok()); + pub fn verify_digest( + &self, + message_digest: Sha512, + context: Context, + signature: &Signature, + ) -> Result<(), SignatureError> { + self.0 + .verify_prehashed_strict(message_digest, Some(context.0), signature) + } +} + +impl TryFrom for VerifyingKey { + type Error = TryFromBytesError; + + fn try_from(value: ed25519_dalek::VerifyingKey) -> Result { + Self::try_from_bytes(value.as_bytes()) + } +} + +/// An ed25519 private key. Used for signing messages. +#[derive(Debug)] +pub struct SigningKey(ed25519_dalek::SigningKey); + +impl SigningKey { + pub const LEN: usize = Self::key_len(); + + pub fn from_bytes(bytes: &[u8; Self::LEN]) -> Self { + let signing = ed25519_dalek::SigningKey::from_bytes(bytes); + let _pub = VerifyingKey::try_from(signing.verifying_key()) + .expect("this should never fail. if it does, please open an issue"); + Self(signing) + } + + // TODO: Turn this into inline const when that feature stabilizes + const fn key_len() -> usize { + let len = crate::key_algos::Ed25519::SIGNING_KEY_LEN; + assert!(len == ed25519_dalek::SECRET_KEY_LENGTH); + len + } + + pub fn into_inner(self) -> ed25519_dalek::SigningKey { + self.0 + } + + /// Generates a new random [`SigningKey`]. + #[cfg(feature = "random")] + pub fn random() -> Self { + let mut csprng = rand_core::OsRng; + Self(ed25519_dalek::SigningKey::generate(&mut csprng)) + } + + /// Generates a new random [`SigningKey`] from the given RNG. + #[cfg(feature = "random")] + pub fn random_from_rng(rng: &mut R) -> Self { + Self(ed25519_dalek::SigningKey::generate(rng)) + } + + /// Gets the public [`VerifyingKey`] that corresponds to this private [`SigningKey`]. + pub fn verifying_key(&self) -> VerifyingKey { + VerifyingKey::try_from(self.0.verifying_key()) + .expect("this should never fail. if it does, please open an issue") + } + + /// Signs `message` using the ed25519ph algorithm. + /// + /// # Example + /// + /// ``` + /// use did_simple::crypto::{Context, ed25519::{SigningKey, VerifyingKey}}; + /// + /// let signing_key = SigningKey::random(); + /// let verifying_key = signing_key.verifying_key(); + /// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes()); + /// + /// let msg = "everyone can read and verify this message"; + /// let sig = signing_key.sign(msg, CTX); + /// + /// assert!(verifying_key.verify(msg, CTX, &sig).is_ok()); + /// ``` + pub fn sign(&self, message: impl AsRef<[u8]>, context: Context) -> Signature { + let digest = Sha512::new().chain_update(message); + self.sign_digest(digest, context) + } + + /// Same as `sign`, but allows you to populate `message_digest` separately + /// from signing. + /// + /// This can be useful if for example, it is undesirable to buffer the message + /// into a single slice, or the message is being streamed asynchronously. + /// You can instead update the digest chunk by chunk, and pass the digest + /// in after you are done reading all the data. + /// + /// # Example + /// + /// ``` + /// use did_simple::crypto::{Context, ed25519::{Sha512, Digest, SigningKey, VerifyingKey}}; + /// + /// let signing_key = SigningKey::random(); + /// let verifying_key = signing_key.verifying_key(); + /// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes()); + /// + /// let mut digest = Sha512::new(); + /// digest.update("this is "); + /// digest.update("my message"); + /// let sig = signing_key.sign_digest(digest, CTX); + /// + /// assert!(verifying_key.verify("this is my message", CTX, &sig).is_ok()); + pub fn sign_digest(&self, message_digest: Sha512, context: Context) -> Signature { + self.0 + .sign_prehashed(message_digest, Some(context.0)) + .expect("this should never fail. if it does, please open an issue") + } } #[derive(thiserror::Error, Debug)] @@ -44,7 +221,5 @@ pub enum TryFromBytesError { WeakKey, } -/// Errors which may occur while processing signatures and keypairs. -#[derive(thiserror::Error, Debug)] -#[error("invalid signature")] -pub struct SignatureError(#[from] ed25519_dalek::SignatureError); +#[cfg(test)] +mod test {} diff --git a/crates/did-simple/src/crypto/mod.rs b/crates/did-simple/src/crypto/mod.rs index c9fa1c4..579e646 100644 --- a/crates/did-simple/src/crypto/mod.rs +++ b/crates/did-simple/src/crypto/mod.rs @@ -1,4 +1,164 @@ //! Implementations of cryptographic operations +// Re-exports +#[cfg(feature = "random")] +pub use rand_core; + #[cfg(feature = "ed25519")] pub mod ed25519; + +/// The "context" for signing and verifying messages, which is used for domain +/// separation of message signatures. The context can be of length +/// `[Context::MIN_LEN..Context::MAX_LEN]`. +/// +/// # Example +/// +/// ``` +/// use did_simple::crypto::Context; +/// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes()); +/// ``` +/// +/// # What is the purpose of this? +/// +/// Messages signed using one context cannot be verified under a different +/// context. This is important, because it prevents tricking someone into +/// signing a message for one use case, and it getting reused for another use +/// case. +/// +/// # Can you give me an example of how not using a context can be bad? +/// +/// Suppose that a scammer sends you a file and asks you to send it back to them +/// signed, to prove to them that you got the message. You naively comply, only +/// later to realize that the file you signed actually is a json message that +/// authorizes your bank to send funds to the scammer. If you reused the same +/// public key for sending messages as you do for authorizing bank transactions, +/// you just got robbed. +/// +/// If instead the application you were using signed that message with a +/// "MySecureProtocolSendMessage" context, and your bank used "MySuperSafeBank", +/// your bank would have rejected the message signature when the scammer tried +/// to use it to authorize a funds transfer because the two contexts don't +/// match. +/// +/// # But I *really* need to not use a context for this specific case 🥺 +/// +/// Most of the signing algorithms' `VerifyingKey`s expose an `into_inner` +/// method and reexport the cryptography crate they use. So you can just call +/// the relevant signing functions yourself with the lower level crate. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct Context<'a>(&'a [u8]); + +/// Helper just to allow same value in macro contexts as const contexts +macro_rules! ctx_len { + ("max") => { + 255 + }; + ("min") => { + 4 + }; +} + +impl<'a> Context<'a> { + pub const MAX_LEN: usize = Self::max_len(); + pub const MIN_LEN: usize = Self::min_len(); + + /// Panics if `value` is longer than [`Self::MAX_LEN`] or is 0. + pub const fn from_bytes(value: &'a [u8]) -> Self { + match Self::try_from_bytes(value) { + Ok(ctx) => ctx, + Err(err) => panic!("{}", err.const_display()), + } + } + + pub const fn try_from_bytes(value: &'a [u8]) -> Result { + let len = value.len(); + if len < Self::MIN_LEN { + Err(ContextError::SliceTooShort) + } else if len > Self::MAX_LEN { + Err(ContextError::SliceTooLong(len)) + } else { + Ok(Context(value)) + } + } + + const fn max_len() -> usize { + let result = ed25519_dalek::Context::::MAX_LENGTH; + assert!(result == ctx_len!("max")); + result + } + + const fn min_len() -> usize { + let result = ctx_len!("min"); + assert!(result <= Self::MAX_LEN); + result + } +} + +impl<'a> TryFrom<&'a [u8]> for Context<'a> { + type Error = ContextError; + + fn try_from(value: &'a [u8]) -> Result { + Self::try_from_bytes(value) + } +} + +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +pub enum ContextError { + #[error( + "requires a slice of at least length {} but got a slice of length {0}", + Context::MIN_LEN + )] + SliceTooShort, + #[error( + "requires a slice of at most length {} but got a slice of length {0}", + Context::MAX_LEN + )] + SliceTooLong(usize), +} + +impl ContextError { + const fn const_display(&self) -> &str { + match self { + Self::SliceTooShort => { + concat!("requires a slice of at least length ", ctx_len!("min")) + } + Self::SliceTooLong(_len) => { + concat!("requires a slice of at most length ", ctx_len!("max")) + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ctx_try_from() { + let valid = [ + [0; Context::MIN_LEN].as_slice(), + [0; Context::MAX_LEN].as_slice(), + ]; + for s in valid { + assert_eq!(Context::from_bytes(s), Context::try_from_bytes(s).unwrap()); + } + + let too_short = [&[], [0; Context::MIN_LEN - 1].as_slice()]; + for s in too_short { + assert_eq!(Context::try_from_bytes(s), Err(ContextError::SliceTooShort)); + assert!(std::panic::catch_unwind(|| Context::from_bytes(s)).is_err()); + } + + let too_long = [ + [0; Context::MAX_LEN + 1].as_slice(), + [0; Context::MAX_LEN + 2].as_slice(), + ]; + for s in too_long { + assert_eq!( + Context::try_from_bytes(s), + Err(ContextError::SliceTooLong(s.len())) + ); + assert!(std::panic::catch_unwind(|| Context::from_bytes(s)).is_err()); + } + } +} diff --git a/crates/did-simple/src/key_algos.rs b/crates/did-simple/src/key_algos.rs index eff2b59..bc75582 100644 --- a/crates/did-simple/src/key_algos.rs +++ b/crates/did-simple/src/key_algos.rs @@ -6,18 +6,27 @@ pub enum KeyAlgo { } impl KeyAlgo { - pub fn pub_key_len(&self) -> usize { + pub fn verifying_key_len(&self) -> usize { match self { - Self::Ed25519 => Ed25519::PUB_KEY_LEN, + Self::Ed25519 => Ed25519::VERIFYING_KEY_LEN, + } + } + + pub fn signing_key_len(&self) -> usize { + match self { + Self::Ed25519 => Ed25519::SIGNING_KEY_LEN, } } } // ---- internal code ---- -/// A key algorithm that is known statically, at compile time. -pub(crate) trait StaticKeyAlgo { - const PUB_KEY_LEN: usize; +/// A signing algorithm that is known statically, at compile time. +pub(crate) trait StaticSigningAlgo { + /// The length of the public verifying key. + const VERIFYING_KEY_LEN: usize; + /// The length of the private signing key. + const SIGNING_KEY_LEN: usize; const MULTICODEC_VALUE: u16; const MULTICODEC_VALUE_ENCODED: &'static [u8] = encode_varint(Self::MULTICODEC_VALUE).as_slice(); @@ -26,8 +35,9 @@ pub(crate) trait StaticKeyAlgo { #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] pub(crate) struct Ed25519; -impl StaticKeyAlgo for Ed25519 { - const PUB_KEY_LEN: usize = 32; +impl StaticSigningAlgo for Ed25519 { + const VERIFYING_KEY_LEN: usize = 32; + const SIGNING_KEY_LEN: usize = 32; const MULTICODEC_VALUE: u16 = 0xED; } diff --git a/crates/did-simple/src/methods/key.rs b/crates/did-simple/src/methods/key.rs index 85f7a05..73f8df4 100644 --- a/crates/did-simple/src/methods/key.rs +++ b/crates/did-simple/src/methods/key.rs @@ -5,7 +5,7 @@ use std::fmt::Display; use crate::{ - key_algos::{Ed25519, KeyAlgo, StaticKeyAlgo}, + key_algos::{Ed25519, KeyAlgo, StaticSigningAlgo}, url::{DidMethod, DidUrl}, utf8bytes::Utf8Bytes, varint::decode_varint, @@ -54,7 +54,7 @@ impl DidKey { let result = match self.key_algo { KeyAlgo::Ed25519 => &self.mb_value[self.pubkey_bytes.clone()], }; - debug_assert_eq!(result.len(), self.key_algo.pub_key_len()); + debug_assert_eq!(result.len(), self.key_algo.verifying_key_len()); result } } @@ -108,7 +108,7 @@ impl TryFrom for DidKey { // tail bytes will end up being the pubkey bytes if everything passes validation let (multicodec_key_algo, tail_bytes) = decode_varint(&decoded_multibase)?; let (key_algo, pub_key_len) = match multicodec_key_algo { - Ed25519::MULTICODEC_VALUE => (KeyAlgo::Ed25519, Ed25519::PUB_KEY_LEN), + Ed25519::MULTICODEC_VALUE => (KeyAlgo::Ed25519, Ed25519::SIGNING_KEY_LEN), _ => return Err(FromUrlError::UnknownKeyAlgo(multicodec_key_algo)), }; @@ -137,7 +137,7 @@ pub enum FromUrlError { UnknownKeyAlgo(u16), #[error(transparent)] Varint(#[from] crate::varint::DecodeError), - #[error("{0:?} requires pubkeys of length {} but got {1} bytes", .0.pub_key_len())] + #[error("{0:?} requires pubkeys of length {} but got {1} bytes", .0.verifying_key_len())] MismatchedPubkeyLen(KeyAlgo, usize), }