crates/gunnar-grpc/tests/security_key_common/mod.rs
//! A FIDO2 authenticator in software, for `sk-ssh-ed25519@openssh.com`: what
//! a security key computes when OpenSSH asks it to sign — ed25519 over
//! `SHA256(application) ‖ flags ‖ counter ‖ SHA256(message)`, the flags byte
//! carrying user presence (0x01) when somebody touched it.
#![allow(dead_code)]
use ed25519_dalek::Signer as _;
use nornir_auth_sshsig::ssh_key;
use sha2::{Digest as _, Sha256};
pub const APPLICATION: &str = "ssh:";
pub const USER_PRESENT: u8 = 0x01;
pub struct SecurityKey {
signing: ed25519_dalek::SigningKey,
pub counter: u32,
}
impl SecurityKey {
pub fn new(seed: u8) -> SecurityKey {
SecurityKey {
signing: ed25519_dalek::SigningKey::from_bytes(&[seed; 32]),
counter: 0,
}
}
/// The `sk-ssh-ed25519@openssh.com` public key, as ssh-key 0.6 (the
/// sshsig crate's) holds it.
pub fn key_data(&self) -> ssh_key::public::KeyData {
ssh_key::public::KeyData::SkEd25519(ssh_key::public::SkEd25519::new(
ssh_key::public::Ed25519PublicKey(self.signing.verifying_key().to_bytes()),
APPLICATION.to_owned(),
))
}
/// The OpenSSH public key line gunnar's key table holds.
pub fn openssh(&self) -> String {
ssh_key::PublicKey::new(self.key_data(), "kim@yubikey")
.to_openssh()
.expect("encodes")
}
pub fn fingerprint(&self) -> String {
ssh_key::PublicKey::new(self.key_data(), "")
.fingerprint(ssh_key::HashAlg::Sha256)
.to_string()
}
/// The SSH signature blob the authenticator answers `data` with — `data`
/// being what OpenSSH hands it (for sshsig, `SshSig::signed_data`).
pub fn sign(&mut self, data: &[u8], flags: u8) -> ssh_key::Signature {
self.counter += 1;
let mut message = Sha256::digest(APPLICATION.as_bytes()).to_vec();
message.push(flags);
message.extend_from_slice(&self.counter.to_be_bytes());
message.extend_from_slice(&Sha256::digest(data));
let mut blob = self.signing.sign(&message).to_bytes().to_vec();
blob.push(flags);
blob.extend_from_slice(&self.counter.to_be_bytes());
ssh_key::Signature::new(ssh_key::Algorithm::SkEd25519, blob).expect("an sk signature")
}
/// A whole gunnar credential, signed by this key with `flags`.
pub fn credential(
&mut self,
issued_at_unix_ms: i64,
nonce: &str,
method: &str,
flags: u8,
) -> gunnar_grpc::auth::Credential {
let fingerprint = self.fingerprint();
let message = nornir_auth_sshsig::challenge_bound::<gunnar_grpc::auth::GunnarScheme>(
&fingerprint,
issued_at_unix_ms,
nonce,
method,
);
let signed = ssh_key::SshSig::signed_data(
gunnar_grpc::auth::SSHSIG_NAMESPACE,
ssh_key::HashAlg::Sha512,
message.as_bytes(),
)
.expect("signed data");
let signature = self.sign(&signed, flags);
let sshsig = ssh_key::SshSig::new(
self.key_data(),
gunnar_grpc::auth::SSHSIG_NAMESPACE,
ssh_key::HashAlg::Sha512,
signature,
)
.expect("an sshsig");
let armor = sshsig.to_pem(ssh_key::LineEnding::LF).expect("armours");
gunnar_grpc::auth::Credential::new(
fingerprint,
issued_at_unix_ms,
nonce.to_owned(),
Some(method.to_owned()),
nornir_auth_sshsig::armor_to_line(&armor),
)
}
}