Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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),
        )
    }
}