use alloc::vec::Vec;
use purecrypto::ec::{BoxedEcdsaPublicKey, Ed25519PublicKey, Ed25519Signature};
use purecrypto::hash::{Digest, Sha256};
use super::HostKeyVerify;
use crate::error::{Error, Result};
use crate::format::{Reader, Writer};
pub const SK_ECDSA: &str = "sk-ecdsa-sha2-nistp256@openssh.com";
pub const SK_ED25519: &str = "sk-ssh-ed25519@openssh.com";
pub const WEBAUTHN_SK_ECDSA: &str = "webauthn-sk-ecdsa-sha2-nistp256@openssh.com";
pub const SK_USER_PRESENCE: u8 = 0x01;
pub const SK_USER_VERIFICATION: u8 = 0x04;
const WEBAUTHN_AD: u8 = 0x40;
const WEBAUTHN_ED: u8 = 0x80;
pub fn sk_signature_flags(sig_blob: &[u8]) -> Option<u8> {
let mut r = Reader::new(sig_blob);
let name = r.read_string().ok()?;
if name != SK_ECDSA.as_bytes()
&& name != SK_ED25519.as_bytes()
&& name != WEBAUTHN_SK_ECDSA.as_bytes()
{
return None;
}
r.read_string().ok()?;
r.read_u8().ok()
}
pub fn sk_signature_details(sig_blob: &[u8]) -> Option<(u8, u32)> {
sk_signature_flags(sig_blob)?;
let mut r = Reader::new(sig_blob);
r.read_string().ok()?;
r.read_string().ok()?;
Some((r.read_u8().ok()?, r.read_u32().ok()?))
}
pub fn is_sk_algorithm(name: &str) -> bool {
name.starts_with("sk-") || name.starts_with("webauthn-sk-")
}
fn signed_data(
application: &[u8],
flags: u8,
counter: u32,
exts: &[u8],
msghash: &[u8],
) -> Vec<u8> {
let mut w = Writer::new();
w.write_raw(Sha256::digest(application).as_ref());
w.write_u8(flags);
w.write_u32(counter);
w.write_raw(exts);
w.write_raw(msghash);
w.into_vec()
}
fn read_application<'a>(r: &mut Reader<'a>) -> Result<&'a [u8]> {
let app = r.read_string()?;
if !r.is_empty() {
return Err(Error::Format("sk: public key trailing data"));
}
Ok(app)
}
pub struct SkEcdsaP256Key {
public: BoxedEcdsaPublicKey,
application: Vec<u8>,
}
impl SkEcdsaP256Key {
pub fn application(&self) -> &[u8] {
&self.application
}
}
impl HostKeyVerify for SkEcdsaP256Key {
fn algorithm(&self) -> &'static str {
SK_ECDSA
}
fn verify(&self, msg: &[u8], sig_blob: &[u8]) -> Result<()> {
let mut r = Reader::new(sig_blob);
let name = r.read_string()?;
let webauthn = if name == WEBAUTHN_SK_ECDSA.as_bytes() {
true
} else if name == SK_ECDSA.as_bytes() {
false
} else {
return Err(Error::Format("sk-ecdsa: signature algorithm mismatch"));
};
let inner = r.read_string()?;
let flags = r.read_u8()?;
let counter = r.read_u32()?;
let (msghash, exts) = if webauthn {
let origin = r.read_string()?;
let client_data = r.read_string()?;
let exts = r.read_string()?;
(webauthn_hash(msg, origin, client_data, flags, exts)?, exts)
} else {
(Sha256::digest(msg).as_ref().to_vec(), &[][..])
};
if !r.is_empty() {
return Err(Error::Format("sk-ecdsa: signature trailing data"));
}
let signed = signed_data(&self.application, flags, counter, exts, &msghash);
super::ecdsa::verify_p256_inner(&self.public, &signed, inner)
}
fn from_public_blob(blob: &[u8]) -> Result<Self> {
let mut r = Reader::new(blob);
if r.read_string()? != SK_ECDSA.as_bytes() {
return Err(Error::Format("sk-ecdsa: public key type mismatch"));
}
if r.read_string()? != b"nistp256" {
return Err(Error::Format("sk-ecdsa: curve identifier mismatch"));
}
let public = super::ecdsa::p256_from_sec1(r.read_string()?)?;
let application = read_application(&mut r)?.to_vec();
Ok(Self {
public,
application,
})
}
}
fn webauthn_hash(
msg: &[u8],
origin: &[u8],
client_data: &[u8],
flags: u8,
exts: &[u8],
) -> Result<Vec<u8>> {
if origin.contains(&b'"')
|| flags & WEBAUTHN_AD != 0
|| (flags & WEBAUTHN_ED == 0) != exts.is_empty()
{
return Err(Error::Format("sk-ecdsa: inconsistent webauthn signature"));
}
let mut want = Vec::new();
want.extend_from_slice(b"{\"type\":\"webauthn.get\",\"challenge\":\"");
base64url_nopad(msg, &mut want);
want.extend_from_slice(b"\",\"origin\":\"");
want.extend_from_slice(origin);
want.push(b'"');
if !client_data.starts_with(&want) {
return Err(Error::BadSignature);
}
Ok(Sha256::digest(client_data).as_ref().to_vec())
}
fn base64url_nopad(data: &[u8], out: &mut Vec<u8>) {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
for chunk in data.chunks(3) {
let n = chunk
.iter()
.enumerate()
.fold(0u32, |n, (i, &b)| n | u32::from(b) << (16 - 8 * i));
for i in 0..=chunk.len() {
out.push(ALPHABET[(n >> (18 - 6 * i) & 63) as usize]);
}
}
}
pub struct SkEd25519Key {
public: Ed25519PublicKey,
application: Vec<u8>,
}
impl SkEd25519Key {
pub fn application(&self) -> &[u8] {
&self.application
}
}
impl HostKeyVerify for SkEd25519Key {
fn algorithm(&self) -> &'static str {
SK_ED25519
}
fn verify(&self, msg: &[u8], sig_blob: &[u8]) -> Result<()> {
let mut r = Reader::new(sig_blob);
if r.read_string()? != SK_ED25519.as_bytes() {
return Err(Error::Format("sk-ed25519: signature algorithm mismatch"));
}
let raw = r.read_string()?;
let flags = r.read_u8()?;
let counter = r.read_u32()?;
if !r.is_empty() {
return Err(Error::Format("sk-ed25519: signature trailing data"));
}
let raw: [u8; 64] = raw
.try_into()
.map_err(|_| Error::Format("sk-ed25519: signature length"))?;
let signed = signed_data(
&self.application,
flags,
counter,
&[],
Sha256::digest(msg).as_ref(),
);
self.public
.verify(&signed, &Ed25519Signature::from_bytes(raw))
.map_err(|_| Error::BadSignature)
}
fn from_public_blob(blob: &[u8]) -> Result<Self> {
let mut r = Reader::new(blob);
if r.read_string()? != SK_ED25519.as_bytes() {
return Err(Error::Format("sk-ed25519: public key type mismatch"));
}
let pk: [u8; 32] = r
.read_string()?
.try_into()
.map_err(|_| Error::Format("sk-ed25519: public key length"))?;
let application = read_application(&mut r)?.to_vec();
Ok(Self {
public: Ed25519PublicKey::from_bytes(pk),
application,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "std")]
fn fixture(name: &str) -> std::string::String {
let path = std::format!("{}/tests/fixtures/sk/{name}", env!("CARGO_MANIFEST_DIR"));
std::fs::read_to_string(path).unwrap()
}
#[cfg(feature = "std")]
#[test]
fn openssh_signatures_verify() {
let msg = fixture("msg");
for t in ["ecdsa-sk", "ed25519-sk"] {
let sig =
crate::sshsig::Signature::from_armored(&fixture(&std::format!("{t}.sig"))).unwrap();
let key = crate::key::PublicKey::parse_authorized_keys_line(&fixture(&std::format!(
"{t}.pub"
)))
.unwrap();
assert_eq!(sig.public_key, key.wire_blob(), "{t}");
sig.verify("file", &sig.hash.digest(msg.as_bytes()))
.unwrap();
assert!(
sig.verify("file", &sig.hash.digest(b"other")).is_err(),
"{t}"
);
assert!(
sig.verify("git", &sig.hash.digest(msg.as_bytes())).is_err(),
"{t}"
);
}
}
#[cfg(feature = "std")]
#[test]
fn openssh_signature_flags() {
for t in ["ecdsa-sk", "ed25519-sk"] {
let sig =
crate::sshsig::Signature::from_armored(&fixture(&std::format!("{t}.sig"))).unwrap();
let flags = sk_signature_flags(&sig.signature).unwrap();
assert_eq!(flags & SK_USER_PRESENCE, SK_USER_PRESENCE, "{t}");
}
}
#[cfg(feature = "std")]
#[test]
fn openssh_key_files_and_certificates() {
for t in ["ecdsa-sk", "ed25519-sk"] {
let pk = crate::key::PrivateKey::parse_openssh_pem(&fixture(t), None).unwrap();
let public = crate::key::PublicKey::parse_authorized_keys_line(&fixture(
&std::format!("{t}.pub"),
))
.unwrap();
assert_eq!(pk.public_key().wire_blob(), public.wire_blob(), "{t}");
let sk = pk.sk_credential().unwrap();
assert_eq!(sk.application, "ssh:");
assert!(!sk.key_handle.is_empty());
let written = pk.to_openssh_pem(None).unwrap();
let again = crate::key::PrivateKey::parse_openssh_pem(&written, None).unwrap();
assert_eq!(again.sk_credential(), pk.sk_credential(), "{t}");
assert_eq!(again.public_key().wire_blob(), public.wire_blob(), "{t}");
let cert = crate::cert::Certificate::parse_openssh_line(&fixture(&std::format!(
"{t}-cert.pub"
)))
.unwrap();
assert_eq!(cert.embedded_pubkey_blob(), public.wire_blob(), "{t}");
cert.require_known_critical_options().unwrap();
assert!(cert.critical_option("verify-required").is_some());
assert!(
cert.extensions
.iter()
.any(|(n, _)| n == "no-touch-required")
);
cert.verify_ca_signature(crate::config::algos::CA_SIGNATURE_DEFAULTS)
.unwrap();
}
}
#[test]
fn base64url_matches_rfc4648() {
for (input, want) in [
(&b""[..], ""),
(b"f", "Zg"),
(b"fo", "Zm8"),
(b"foo", "Zm9v"),
(b"foob", "Zm9vYg"),
(b"\xfb\xff", "-_8"),
] {
let mut out = Vec::new();
base64url_nopad(input, &mut out);
assert_eq!(out, want.as_bytes(), "{input:?}");
}
}
#[test]
fn flags_only_for_sk_signatures() {
let mut w = Writer::new();
w.write_string(SK_ED25519.as_bytes());
w.write_string(&[0u8; 64]);
w.write_u8(SK_USER_PRESENCE | SK_USER_VERIFICATION);
w.write_u32(7);
assert_eq!(sk_signature_flags(w.as_slice()), Some(0x05));
let mut w = Writer::new();
w.write_string(b"ssh-ed25519");
w.write_string(&[0u8; 64]);
assert_eq!(sk_signature_flags(w.as_slice()), None);
}
}