From 78282879a91afb516510f2c3b7346a4156a10db3 Mon Sep 17 00:00:00 2001 From: Alessio Di Mauro Date: Tue, 1 Nov 2022 17:23:28 +0100 Subject: [PATCH] ssh-key: add partial support for U2F signatures verification Initial work to add support for verifying `sk-ssh-ed25519` signatures. --- ssh-key/src/signature.rs | 78 ++++++++++++++++++++-- ssh-key/src/sshsig.rs | 4 ++ ssh-key/tests/examples/id_sk_ed25519_2.pub | 1 + ssh-key/tests/examples/sshsig_sk_ed25519 | 7 ++ ssh-key/tests/sshsig.rs | 59 ++++++++++++++++ 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 ssh-key/tests/examples/id_sk_ed25519_2.pub create mode 100644 ssh-key/tests/examples/sshsig_sk_ed25519 diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index 83c339d..6856e55 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -7,7 +7,10 @@ use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use signature::{Signer, Verifier}; #[cfg(feature = "ed25519")] -use crate::{private::Ed25519Keypair, public::Ed25519PublicKey}; +use { + crate::{private::Ed25519Keypair, public::Ed25519PublicKey}, + sha2::{Digest, Sha256}, +}; #[cfg(feature = "dsa")] use { @@ -31,6 +34,8 @@ use { const DSA_SIGNATURE_SIZE: usize = 40; const ED25519_SIGNATURE_SIZE: usize = 64; +const SK_ED25519_SIGNATURE_TRAILER_SIZE: usize = 5; // flags(u8), counter(u32) +const SK_ED25519_SIGNATURE_SIZE: usize = ED25519_SIGNATURE_SIZE + SK_ED25519_SIGNATURE_TRAILER_SIZE; /// Trait for signing keys which produce a [`Signature`]. /// @@ -115,6 +120,7 @@ impl Signature { } } Algorithm::Ed25519 if data.len() == ED25519_SIGNATURE_SIZE => (), + Algorithm::SkEd25519 if data.len() == SK_ED25519_SIGNATURE_SIZE => (), Algorithm::Rsa { hash: Some(_) } => (), _ => return Err(encoding::Error::Length.into()), } @@ -159,7 +165,16 @@ impl Decode for Signature { fn decode(reader: &mut impl Reader) -> Result { let algorithm = Algorithm::decode(reader)?; - let data = Vec::decode(reader)?; + let mut data = Vec::decode(reader)?; + + if algorithm == Algorithm::SkEd25519 { + let flags = u8::decode(reader)?; + let counter = u32::decode(reader)?; + + data.push(flags); + data.extend(counter.to_be_bytes()); + } + Self::new(algorithm, data) } } @@ -181,7 +196,19 @@ impl Encode for Signature { } self.algorithm().encode(writer)?; - self.as_bytes().encode(writer)?; + + if self.algorithm == Algorithm::SkEd25519 { + let signature_length = self + .as_bytes() + .len() + .checked_sub(SK_ED25519_SIGNATURE_TRAILER_SIZE) + .ok_or(encoding::Error::Length)?; + self.as_bytes()[..signature_length].encode(writer)?; + writer.write(&self.as_bytes()[signature_length..])?; + } else { + self.as_bytes().encode(writer)?; + } + Ok(()) } } @@ -268,6 +295,8 @@ impl Verifier for public::KeyData { Self::Ecdsa(pk) => pk.verify(message, signature), #[cfg(feature = "ed25519")] Self::Ed25519(pk) => pk.verify(message, signature), + #[cfg(feature = "ed25519")] + Self::SkEd25519(pk) => pk.verify(message, signature), #[cfg(feature = "rsa")] Self::Rsa(pk) => pk.verify(message, signature), _ => Err(signature::Error::new()), @@ -324,7 +353,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature { fn try_from(signature: &Signature) -> Result { match signature.algorithm { - Algorithm::Ed25519 => Ok(ed25519_dalek::Signature::try_from(signature.as_bytes())?), + Algorithm::Ed25519 | Algorithm::SkEd25519 => { + Ok(ed25519_dalek::Signature::try_from(signature.as_bytes())?) + } _ => Err(Error::Algorithm), } } @@ -352,6 +383,29 @@ impl Verifier for Ed25519PublicKey { } } +#[cfg(feature = "ed25519")] +#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))] +impl Verifier for public::SkEd25519 { + fn verify(&self, message: &[u8], signature: &Signature) -> signature::Result<()> { + let signature_len = signature + .as_bytes() + .len() + .checked_sub(SK_ED25519_SIGNATURE_TRAILER_SIZE) + .ok_or(Error::Encoding(encoding::Error::Length))?; + let signature_bytes = &signature.as_bytes()[..signature_len]; + let flags_and_counter = &signature.as_bytes()[signature_len..]; + + let mut signed_data = + Vec::with_capacity((2 * Sha256::output_size()) + SK_ED25519_SIGNATURE_TRAILER_SIZE); + signed_data.extend(Sha256::digest(self.application())); + signed_data.extend(flags_and_counter); + signed_data.extend(Sha256::digest(message)); + + let signature = ed25519_dalek::Signature::try_from(signature_bytes)?; + ed25519_dalek::PublicKey::try_from(self.public_key())?.verify(&signed_data, &signature) + } +} + #[cfg(feature = "p256")] #[cfg_attr(docsrs, doc(cfg(feature = "p256")))] impl TryFrom for Signature { @@ -618,6 +672,7 @@ mod tests { const DSA_SIGNATURE: &[u8] = &hex!("000000077373682d6473730000002866725bf3c56100e975e21fff28a60f73717534d285ea3e1beefc2891f7189d00bd4d94627e84c55c"); const ECDSA_SHA2_P256_SIGNATURE: &[u8] = &hex!("0000001365636473612d736861322d6e6973747032353600000048000000201298ab320720a32139cda8a40c97a13dc54ce032ea3c6f09ea9e87501e48fa1d0000002046e4ac697a6424a9870b9ef04ca1182cd741965f989bd1f1f4a26fd83cf70348"); const ED25519_SIGNATURE: &[u8] = &hex!("0000000b7373682d65643235353139000000403d6b9906b76875aef1e7b2f1e02078a94f439aebb9a4734da1a851a81e22ce0199bbf820387a8de9c834c9c3cc778d9972dcbe70f68d53cc6bc9e26b02b46d04"); + const SK_ED25519_SIGNATURE: &[u8] = &hex!("0000001a736b2d7373682d65643235353139406f70656e7373682e636f6d000000402f5670b6f93465d17423878a74084bf331767031ed240c627c8eb79ab8fa1b935a1fd993f52f5a13fec1797f8a434f943a6096246aea8dd5c8aa922cba3d95060100000009"); const RSA_SHA512_SIGNATURE: &[u8] = &hex!("0000000c7273612d736861322d3531320000018085a4ad1a91a62c00c85de7bb511f38088ff2bce763d76f4786febbe55d47624f9e2cffce58a680183b9ad162c7f0191ea26cab001ac5f5055743eced58e9981789305c208fc98d2657954e38eb28c7e7f3fbe92393a14324ed77aebb772a41aa7a107b38cb9bd1d9ad79b275135d1d7e019bb1d56d74f2450be6db0771f48f6707d3fcf9789592ca2e55595acc16b6e8d0139b56c5d1360b3a1e060f4151a3d7841df2c2a8c94d6f8a1bf633165ee0bcadac5642763df0dd79d3235ae5506595145f199d8abe8f9980411bf70a16e30f273736324d047043317044c36374d6a5ed34cac251e01c6795e4578393f9090bf4ae3e74a0009275a197315fc9c62f1c9aec1ba3b2d37c3b207e5500df19e090e7097ebc038fb9c9e35aea9161479ba6b5190f48e89e1abe51e8ec0e120ef89776e129687ca52d1892c8e88e6ef062a7d96b8a87682ca6a42ff1df0cdf5815c3645aeed7267ca7093043db0565e0f109b796bf117b9d2bb6d6debc0c67a4c9fb3aae3e29b00c7bd70f6c11cf53c295ff"); /// Example test vector for signing. @@ -647,6 +702,12 @@ mod tests { assert_eq!(Algorithm::Ed25519, signature.algorithm()); } + #[test] + fn decode_sk_ed25519() { + let signature = Signature::try_from(SK_ED25519_SIGNATURE).unwrap(); + assert_eq!(Algorithm::SkEd25519, signature.algorithm()); + } + #[test] fn decode_rsa() { let signature = Signature::try_from(RSA_SHA512_SIGNATURE).unwrap(); @@ -685,6 +746,15 @@ mod tests { assert_eq!(ED25519_SIGNATURE, &result); } + #[test] + fn encode_sk_ed25519() { + let signature = Signature::try_from(SK_ED25519_SIGNATURE).unwrap(); + + let mut result = Vec::new(); + signature.encode(&mut result).unwrap(); + assert_eq!(SK_ED25519_SIGNATURE, &result); + } + #[cfg(feature = "ed25519")] #[test] fn sign_and_verify_ed25519() { diff --git a/ssh-key/src/sshsig.rs b/ssh-key/src/sshsig.rs index a27366f..efd505a 100644 --- a/ssh-key/src/sshsig.rs +++ b/ssh-key/src/sshsig.rs @@ -99,6 +99,10 @@ impl SshSig { return Err(Error::Namespace); } + if signing_key.public_key().is_sk_ed25519() || signing_key.public_key().is_sk_ecdsa_p256() { + return Err(Error::Algorithm); + } + let signed_data = Self::signed_data(namespace, hash_alg, msg)?; let signature = signing_key.try_sign(&signed_data)?; Self::new(signing_key.public_key(), namespace, hash_alg, signature) diff --git a/ssh-key/tests/examples/id_sk_ed25519_2.pub b/ssh-key/tests/examples/id_sk_ed25519_2.pub new file mode 100644 index 0000000..696cf08 --- /dev/null +++ b/ssh-key/tests/examples/id_sk_ed25519_2.pub @@ -0,0 +1 @@ +sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com diff --git a/ssh-key/tests/examples/sshsig_sk_ed25519 b/ssh-key/tests/examples/sshsig_sk_ed25519 new file mode 100644 index 0000000..636deb5 --- /dev/null +++ b/ssh-key/tests/examples/sshsig_sk_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW +V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA +AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG +XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS +LLo9lQYBAAAACQ== +-----END SSH SIGNATURE----- diff --git a/ssh-key/tests/sshsig.rs b/ssh-key/tests/sshsig.rs index 8c0f717..09510e4 100644 --- a/ssh-key/tests/sshsig.rs +++ b/ssh-key/tests/sshsig.rs @@ -40,6 +40,19 @@ const ED25519_SIGNATURE_BYTES: [u8; 64] = hex!( "db06de2e97fafa33fd60928a4fc5a30630aa18020015094af457dc011154150f" ); +/// SkEd25519 OpenSSH-formatted public key. +const SK_ED25519_PUBLIC_KEY: &str = include_str!("examples/id_sk_ed25519_2.pub"); + +/// `sshsig`-encoded signature. +const SK_ED25519_SIGNATURE: &str = include_str!("examples/sshsig_sk_ed25519"); + +/// Bytes of the raw SkEd25519 signature. +const SK_ED25519_SIGNATURE_BYTES: [u8; 69] = hex!( + "2f5670b6f93465d17423878a74084bf331767031ed240c627c8eb79ab8fa1b93" + "5a1fd993f52f5a13fec1797f8a434f943a6096246aea8dd5c8aa922cba3d9506" + "0100000009" +); + /// RSA OpenSSH-formatted private key. #[cfg(feature = "rsa")] const RSA_PRIVATE_KEY: &str = include_str!("examples/id_rsa_3072"); @@ -76,6 +89,27 @@ fn encode_ed25519() { assert_eq!(&sshsig_pem, ED25519_SIGNATURE); } +#[test] +fn decode_sk_ed25519() { + let sshsig = SK_ED25519_SIGNATURE.parse::().unwrap(); + let public_key = SK_ED25519_PUBLIC_KEY.parse::().unwrap(); + + assert_eq!(sshsig.algorithm(), Algorithm::SkEd25519); + assert_eq!(sshsig.version(), 1); + assert_eq!(sshsig.public_key(), public_key.key_data()); + assert_eq!(sshsig.namespace(), NAMESPACE_EXAMPLE); + assert_eq!(sshsig.reserved(), &[]); + assert_eq!(sshsig.hash_alg(), HashAlg::Sha512); + assert_eq!(sshsig.signature_bytes(), SK_ED25519_SIGNATURE_BYTES); +} + +#[test] +fn encode_sk_ed25519() { + let sshsig = SK_ED25519_SIGNATURE.parse::().unwrap(); + let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap(); + assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE); +} + #[test] #[cfg(feature = "dsa")] fn sign_dsa() { @@ -159,3 +193,28 @@ fn verify_ed25519() { Err(Error::Crypto) ); } + +#[test] +#[cfg(feature = "ed25519")] +fn verify_sk_ed25519() { + let verifying_key = SK_ED25519_PUBLIC_KEY.parse::().unwrap(); + let signature = SK_ED25519_SIGNATURE.parse::().unwrap(); + + // valid + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature), + Ok(()) + ); + + // bad namespace + assert_eq!( + verifying_key.verify("bogus namespace", MSG_EXAMPLE, &signature), + Err(Error::Namespace) + ); + + // invalid message + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, b"bogus!", &signature), + Err(Error::Crypto) + ); +}