Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); ssh-key: add partial support for U2F signatures verification by a-dma · Pull Request #44 · RustCrypto/SSH · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ssh-key/src/signature.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ use crate::{private::Ed25519Keypair, public::Ed25519PublicKey};
#[cfg(feature = "dsa")]
use {
crate::{private::DsaKeypair, public::DsaPublicKey},
sha1::{Digest, Sha1},
sha1::Sha1,
signature::{DigestSigner, DigestVerifier, Signature as _},
};

Expand All@@ -26,11 +26,19 @@ use crate::{
#[cfg(feature = "rsa")]
use {
crate::{private::RsaKeypair, public::RsaPublicKey, HashAlg},
sha2::{Sha256, Sha512},
sha2::Sha512,
};

#[cfg(any(feature = "ed25519", feature = "rsa"))]
use sha2::Sha256;

#[cfg(any(feature = "dsa", feature = "ed25519"))]
use sha2::Digest;

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`].
///
Expand DownExpand Up@@ -115,6 +123,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()),
}
Expand DownExpand Up@@ -159,7 +168,16 @@ impl Decode for Signature {

fn decode(reader: &mut impl Reader) -> Result<Self> {
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)
}
}
Expand All@@ -181,7 +199,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(())
}
}
Expand DownExpand Up@@ -268,6 +298,8 @@ impl Verifier<Signature> 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()),
Expand DownExpand Up@@ -324,7 +356,9 @@ impl TryFrom<&Signature> for ed25519_dalek::Signature {

fn try_from(signature: &Signature) -> Result<ed25519_dalek::Signature> {
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),
}
}
Expand DownExpand Up@@ -352,6 +386,30 @@ impl Verifier<Signature> for Ed25519PublicKey {
}
}

#[cfg(feature = "ed25519")]
#[cfg_attr(docsrs, doc(cfg(feature = "ed25519")))]
impl Verifier<Signature> 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..];

#[allow(clippy::integer_arithmetic)]
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<p256::ecdsa::Signature> for Signature {
Expand DownExpand Up@@ -618,6 +676,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.
Expand DownExpand Up@@ -647,6 +706,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();
Expand DownExpand Up@@ -685,6 +750,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() {
Expand Down
9 changes: 9 additions & 0 deletions ssh-key/src/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,15 @@ impl SshSig {
return Err(Error::Namespace);
}

if signing_key.public_key().is_sk_ed25519() {
return Err(Error::Algorithm);
}

#[cfg(feature = "ecdsa")]
if 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)
Expand Down
1 change: 1 addition & 0 deletions ssh-key/tests/examples/id_sk_ed25519_2.pub
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINSoElFleH+nN83FoLqqepJjN+y7Gs5lrn7qXjBqQZyuAAAABHNzaDo= user@example.com
7 changes: 7 additions & 0 deletions ssh-key/tests/examples/sshsig_sk_ed25519
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAg1KgSUW
V4f6c3zcWguqp6kmM37LsazmWufupeMGpBnK4AAAAEc3NoOgAAAAdleGFtcGxlAAAAAAAA
AAZzaGE1MTIAAABnAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAQC9WcLb5NG
XRdCOHinQIS/MxdnAx7SQMYnyOt5q4+huTWh/Zk/UvWhP+wXl/ikNPlDpgliRq6o3VyKqS
LLo9lQYBAAAACQ==
-----END SSH SIGNATURE-----
59 changes: 59 additions & 0 deletions ssh-key/tests/sshsig.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand DownExpand Up@@ -76,6 +89,27 @@ fn encode_ed25519() {
assert_eq!(&sshsig_pem, ED25519_SIGNATURE);
}

#[test]
fn decode_sk_ed25519() {
let sshsig = SK_ED25519_SIGNATURE.parse::<SshSig>().unwrap();
let public_key = SK_ED25519_PUBLIC_KEY.parse::<PublicKey>().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::<SshSig>().unwrap();
let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap();
assert_eq!(&sshsig_pem, SK_ED25519_SIGNATURE);
}

#[test]
#[cfg(feature = "dsa")]
fn sign_dsa() {
Expand DownExpand Up@@ -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::<PublicKey>().unwrap();
let signature = SK_ED25519_SIGNATURE.parse::<SshSig>().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)
);
}