Skip to content

Commit bc0c3d0

Browse files
authored
ssh-key: add partial support for U2F signatures verification (#44)
Initial work to add support for verifying `sk-ssh-ed25519` signatures.
1 parent 348f3fd commit bc0c3d0

5 files changed

Lines changed: 155 additions & 5 deletions

File tree

ssh-key/src/signature.rs

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
1212
#[cfg(feature = "dsa")]
1313
use {
1414
crate::{private::DsaKeypair, public::DsaPublicKey},
15-
sha1::{Digest, Sha1},
15+
sha1::Sha1,
1616
signature::{DigestSigner, DigestVerifier, Signature as _},
1717
};
1818

@@ -26,11 +26,19 @@ use crate::{
2626
#[cfg(feature = "rsa")]
2727
use {
2828
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
29-
sha2::{Sha256, Sha512},
29+
sha2::Sha512,
3030
};
3131

32+
#[cfg(any(feature = "ed25519", feature = "rsa"))]
33+
use sha2::Sha256;
34+
35+
#[cfg(any(feature = "dsa", feature = "ed25519"))]
36+
use sha2::Digest;
37+
3238
const DSA_SIGNATURE_SIZE: usize = 40;
3339
const ED25519_SIGNATURE_SIZE: usize = 64;
40+
const SK_ED25519_SIGNATURE_TRAILER_SIZE: usize = 5; // flags(u8), counter(u32)
41+
const SK_ED25519_SIGNATURE_SIZE: usize = ED25519_SIGNATURE_SIZE + SK_ED25519_SIGNATURE_TRAILER_SIZE;
3442

3543
/// Trait for signing keys which produce a [`Signature`].
3644
///
@@ -115,6 +123,7 @@ impl Signature {
115123
}
116124
}
117125
Algorithm::Ed25519 if data.len() == ED25519_SIGNATURE_SIZE => (),
126+
Algorithm::SkEd25519 if data.len() == SK_ED25519_SIGNATURE_SIZE => (),
118127
Algorithm::Rsa { hash: Some(_) } => (),
119128
_ => return Err(encoding::Error::Length.into()),
120129
}
@@ -159,7 +168,16 @@ impl Decode for Signature {
159168

160169
fn decode(reader: &mut impl Reader) -> Result<Self> {
161170
let algorithm = Algorithm::decode(reader)?;
162-
let data = Vec::decode(reader)?;
171+
let mut data = Vec::decode(reader)?;
172+
173+
if algorithm == Algorithm::SkEd25519 {
174+
let flags = u8::decode(reader)?;
175+
let counter = u32::decode(reader)?;
176+
177+
data.push(flags);
178+
data.extend(counter.to_be_bytes());
179+
}
180+
163181
Self::new(algorithm, data)
164182
}
165183
}
@@ -181,7 +199,19 @@ impl Encode for Signature {
181199
}
182200

183201
self.algorithm().encode(writer)?;
184-
self.as_bytes().encode(writer)?;
202+
203+
if self.algorithm == Algorithm::SkEd25519 {
204+
let signature_length = self
205+
.as_bytes()
206+
.len()
207+
.checked_sub(SK_ED25519_SIGNATURE_TRAILER_SIZE)
208+
.ok_or(encoding::Error::Length)?;
209+
self.as_bytes()[..signature_length].encode(writer)?;
210+
writer.write(&self.as_bytes()[signature_length..])?;
211+
} else {
212+
self.as_bytes().encode(writer)?;
213+
}
214+
185215
Ok(())
186216
}
187217
}
@@ -268,6 +298,8 @@ impl Verifier<Signature> for public::KeyData {
268298
Self::Ecdsa(pk) => pk.verify(message, signature),
269299
#[cfg(feature = "ed25519")]
270300
Self::Ed25519(pk) => pk.verify(message, signature),
301+
#[cfg(feature = "ed25519")]
302+
Self::SkEd25519(pk) => pk.verify(message, signature),
271303
#[cfg(feature = "rsa")]
272304
Self::Rsa(pk) => pk.verify(message, signature),
273305
_ => Err(signature::Error::new()),
@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {
324356

325357
fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
326358
match signature.algorithm {
327-
Algorithm::Ed25519 => Ok(ed25519_dalek::Signature::try_from(signature.as_bytes())?),
359+
Algorithm::Ed25519 | Algorithm::SkEd25519 => {
360+
Ok(ed25519_dalek::Signature::try_from(signature.as_bytes())?)
361+
}
328362
_ => Err(Error::Algorithm),
329363
}
330364
}
@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
352386
}
353387
}
354388

389+
#[cfg(feature = "ed25519")]
390+
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
391+
impl Verifier<Signature> for public::SkEd25519 {
392+
fn verify(&self, message: &[u8], signature: &Signature) -> signature::Result<()> {
393+
let signature_len = signature
394+
.as_bytes()
395+
.len()
396+
.checked_sub(SK_ED25519_SIGNATURE_TRAILER_SIZE)
397+
.ok_or(Error::Encoding(encoding::Error::Length))?;
398+
let signature_bytes = &signature.as_bytes()[..signature_len];
399+
let flags_and_counter = &signature.as_bytes()[signature_len..];
400+
401+
#[allow(clippy::integer_arithmetic)]
402+
let mut signed_data =
403+
Vec::with_capacity((2 * Sha256::output_size()) + SK_ED25519_SIGNATURE_TRAILER_SIZE);
404+
signed_data.extend(Sha256::digest(self.application()));
405+
signed_data.extend(flags_and_counter);
406+
signed_data.extend(Sha256::digest(message));
407+
408+
let signature = ed25519_dalek::Signature::try_from(signature_bytes)?;
409+
ed25519_dalek::PublicKey::try_from(self.public_key())?.verify(&signed_data, &signature)
410+
}
411+
}
412+
355413
#[cfg(feature = "p256")]
356414
#[cfg_attr(docsrs, doc(cfg(feature = "p256")))]
357415
impl TryFrom<p256::ecdsa::Signature> for Signature {
@@ -618,6 +676,7 @@ mod tests {
618676
const DSA_SIGNATURE: &[u8] = &hex!("000000077373682d6473730000002866725bf3c56100e975e21fff28a60f73717534d285ea3e1beefc2891f7189d00bd4d94627e84c55c");
619677
const ECDSA_SHA2_P256_SIGNATURE: &[u8] = &hex!("0000001365636473612d736861322d6e6973747032353600000048000000201298ab320720a32139cda8a40c97a13dc54ce032ea3c6f09ea9e87501e48fa1d0000002046e4ac697a6424a9870b9ef04ca1182cd741965f989bd1f1f4a26fd83cf70348");
620678
const ED25519_SIGNATURE: &[u8] = &hex!("0000000b7373682d65643235353139000000403d6b9906b76875aef1e7b2f1e02078a94f439aebb9a4734da1a851a81e22ce0199bbf820387a8de9c834c9c3cc778d9972dcbe70f68d53cc6bc9e26b02b46d04");
679+
const SK_ED25519_SIGNATURE: &[u8] = &hex!("0000001a736b2d7373682d65643235353139406f70656e7373682e636f6d000000402f5670b6f93465d17423878a74084bf331767031ed240c627c8eb79ab8fa1b935a1fd993f52f5a13fec1797f8a434f943a6096246aea8dd5c8aa922cba3d95060100000009");
621680
const RSA_SHA512_SIGNATURE: &[u8] = &hex!("0000000c7273612d736861322d3531320000018085a4ad1a91a62c00c85de7bb511f38088ff2bce763d76f4786febbe55d47624f9e2cffce58a680183b9ad162c7f0191ea26cab001ac5f5055743eced58e9981789305c208fc98d2657954e38eb28c7e7f3fbe92393a14324ed77aebb772a41aa7a107b38cb9bd1d9ad79b275135d1d7e019bb1d56d74f2450be6db0771f48f6707d3fcf9789592ca2e55595acc16b6e8d0139b56c5d1360b3a1e060f4151a3d7841df2c2a8c94d6f8a1bf633165ee0bcadac5642763df0dd79d3235ae5506595145f199d8abe8f9980411bf70a16e30f273736324d047043317044c36374d6a5ed34cac251e01c6795e4578393f9090bf4ae3e74a0009275a197315fc9c62f1c9aec1ba3b2d37c3b207e5500df19e090e7097ebc038fb9c9e35aea9161479ba6b5190f48e89e1abe51e8ec0e120ef89776e129687ca52d1892c8e88e6ef062a7d96b8a87682ca6a42ff1df0cdf5815c3645aeed7267ca7093043db0565e0f109b796bf117b9d2bb6d6debc0c67a4c9fb3aae3e29b00c7bd70f6c11cf53c295ff");
622681

623682
/// Example test vector for signing.
@@ -647,6 +706,12 @@ mod tests {
647706
assert_eq!(Algorithm::Ed25519, signature.algorithm());
648707
}
649708

709+
#[test]
710+
fn decode_sk_ed25519() {
711+
let signature = Signature::try_from(SK_ED25519_SIGNATURE).unwrap();
712+
assert_eq!(Algorithm::SkEd25519, signature.algorithm());
713+
}
714+
650715
#[test]
651716
fn decode_rsa() {
652717
let signature = Signature::try_from(RSA_SHA512_SIGNATURE).unwrap();
@@ -685,6 +750,15 @@ mod tests {
685750
assert_eq!(ED25519_SIGNATURE, &result);
686751
}
687752

753+
#[test]
754+
fn encode_sk_ed25519() {
755+
let signature = Signature::try_from(SK_ED25519_SIGNATURE).unwrap();
756+
757+
let mut result = Vec::new();
758+
signature.encode(&mut result).unwrap();
759+
assert_eq!(SK_ED25519_SIGNATURE, &result);
760+
}
761+
688762
#[cfg(feature = "ed25519")]
689763
#[test]
690764
fn sign_and_verify_ed25519() {

ssh-key/src/sshsig.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ impl SshSig {
9999
return Err(Error::Namespace);
100100
}
101101

102+
if signing_key.public_key().is_sk_ed25519() {
103+
return Err(Error::Algorithm);
104+
}
105+
106+
#[cfg(feature = "ecdsa")]
107+
if signing_key.public_key().is_sk_ecdsa_p256() {
108+
return Err(Error::Algorithm);
109+
}
110+
102111
let signed_data = Self::signed_data(namespace, hash_alg, msg)?;
103112
let signature = signing_key.try_sign(&signed_data)?;
104113
Self::new(signing_key.public_key(), namespace, hash_alg, signature)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-----BEGIN SSH SIGNATURE-----
2+
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
3+
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
4+
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
5+
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
6+
LLo9lQYBAAAACQ==
7+
-----END SSH SIGNATURE-----

ssh-key/tests/sshsig.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ const ED25519_SIGNATURE_BYTES: [u8; 64] = hex!(
4040
"db06de2e97fafa33fd60928a4fc5a30630aa18020015094af457dc011154150f"
4141
);
4242

43+
/// SkEd25519 OpenSSH-formatted public key.
44+
const SK_ED25519_PUBLIC_KEY: &str = include_str!("examples/id_sk_ed25519_2.pub");
45+
46+
/// `sshsig`-encoded signature.
47+
const SK_ED25519_SIGNATURE: &str = include_str!("examples/sshsig_sk_ed25519");
48+
49+
/// Bytes of the raw SkEd25519 signature.
50+
const SK_ED25519_SIGNATURE_BYTES: [u8; 69] = hex!(
51+
"2f5670b6f93465d17423878a74084bf331767031ed240c627c8eb79ab8fa1b93"
52+
"5a1fd993f52f5a13fec1797f8a434f943a6096246aea8dd5c8aa922cba3d9506"
53+
"0100000009"
54+
);
55+
4356
/// RSA OpenSSH-formatted private key.
4457
#[cfg(feature = "rsa")]
4558
const RSA_PRIVATE_KEY: &str = include_str!("examples/id_rsa_3072");
@@ -76,6 +89,27 @@ fn encode_ed25519() {
7689
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
7790
}
7891

92+
#[test]
93+
fn decode_sk_ed25519() {
94+
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
95+
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().unwrap();
96+
97+
assert_eq!(sshsig.algorithm(), Algorithm::SkEd25519);
98+
assert_eq!(sshsig.version(), 1);
99+
assert_eq!(sshsig.public_key(), public_key.key_data());
100+
assert_eq!(sshsig.namespace(), NAMESPACE_EXAMPLE);
101+
assert_eq!(sshsig.reserved(), &[]);
102+
assert_eq!(sshsig.hash_alg(), HashAlg::Sha512);
103+
assert_eq!(sshsig.signature_bytes(), SK_ED25519_SIGNATURE_BYTES);
104+
}
105+
106+
#[test]
107+
fn encode_sk_ed25519() {
108+
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
109+
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
110+
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
111+
}
112+
79113
#[test]
80114
#[cfg(feature = "dsa")]
81115
fn sign_dsa() {
@@ -159,3 +193,28 @@ fn verify_ed25519() {
159193
Err(Error::Crypto)
160194
);
161195
}
196+
197+
#[test]
198+
#[cfg(feature = "ed25519")]
199+
fn verify_sk_ed25519() {
200+
let verifying_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().unwrap();
201+
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
202+
203+
// valid
204+
assert_eq!(
205+
verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature),
206+
Ok(())
207+
);
208+
209+
// bad namespace
210+
assert_eq!(
211+
verifying_key.verify("bogus namespace", MSG_EXAMPLE, &signature),
212+
Err(Error::Namespace)
213+
);
214+
215+
// invalid message
216+
assert_eq!(
217+
verifying_key.verify(NAMESPACE_EXAMPLE, b"bogus!", &signature),
218+
Err(Error::Crypto)
219+
);
220+
}

0 commit comments

Comments
 (0)