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

nordisk/edda

crates/eid-app/src/lib.rs

//! # eid-app — the shared signing-app flow
//!
//! frida and tyr (and any [`eid_core`]-based eID) sign the **same way** — the same idea as a
//! BankID/Freja sign: the **holder** signs a server-issued [`Challenge`], the **verifier** checks
//! it, and a verified [`Assertion`] comes back. Only the *backends* differ (a WebAuthn holder, a
//! PKCS#11 card, a `tyr` device key; a frida RP vs a tyr verifier). That orchestration is common,
//! so it lives here once instead of being re-written per eID.
//!
//! ```ignore
//! let app = SignApp::new(holder, verifier);          // holder: Authenticator, verifier: Verifier
//! let assertion = app.sign(&challenge).await?;       // authenticate OR sign (challenge decides)
//! ```
#![forbid(unsafe_code)]

use eid_core::{Assertion, AuthError, Authenticator, Challenge, Verifier};

/// Run one full ceremony: the `holder` signs `challenge`, the `verifier` checks it. Returns the
/// verified [`Assertion`] on success. Works for both authentication and signing — the [`Challenge`]
/// (its `data_to_sign`) decides which; the flow is identical. This is the reusable core a frida or
/// tyr signing app is built on.
pub async fn sign<A, V>(
    holder: &A,
    verifier: &V,
    challenge: &Challenge,
) -> Result<Assertion, AuthError>
where
    A: Authenticator + ?Sized,
    V: Verifier + ?Sized,
{
    let assertion = holder.assert(challenge).await?;
    verifier.verify(challenge, &assertion)?;
    Ok(assertion)
}

/// A signing app bound to one holder + one verifier. Call [`sign`](SignApp::sign) per order.
pub struct SignApp<A, V> {
    /// The holder (client side) — WebAuthn / PKCS#11 card / device key.
    pub holder: A,
    /// The verifier (RP / server side).
    pub verifier: V,
}

impl<A, V> SignApp<A, V>
where
    A: Authenticator,
    V: Verifier,
{
    /// Bind a holder + verifier into an app.
    pub fn new(holder: A, verifier: V) -> Self {
        Self { holder, verifier }
    }

    /// Run the ceremony for `challenge` (authenticate or sign) and return the verified assertion.
    pub async fn sign(&self, challenge: &Challenge) -> Result<Assertion, AuthError> {
        sign(&self.holder, &self.verifier, challenge).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use ed25519_dalek::{Signer, SigningKey};
    use eid_core::{HolderKind, SigAlg, signing_input, verify_assertion};

    // A minimal Ed25519 holder + registry verifier — stands in for tyr's device key / frida.
    struct Key(SigningKey);
    #[async_trait]
    impl Authenticator for Key {
        fn kind(&self) -> HolderKind { HolderKind::PlatformKey }
        async fn assert(&self, c: &Challenge) -> Result<Assertion, AuthError> {
            Ok(Assertion {
                kind: HolderKind::PlatformKey,
                alg: SigAlg::Ed25519,
                key_id: self.0.verifying_key().to_bytes().to_vec(),
                signature: self.0.sign(&signing_input(c)).to_bytes().to_vec(),
                attestation: vec![],
            })
        }
    }
    struct Trust(Vec<u8>);
    impl Verifier for Trust {
        fn verify(&self, c: &Challenge, a: &Assertion) -> Result<(), AuthError> {
            if a.key_id != self.0 {
                return Err(AuthError::Verification("untrusted key".into()));
            }
            verify_assertion(c, a, &a.key_id)
        }
    }

    #[tokio::test]
    async fn signs_and_verifies() {
        let key = SigningKey::from_bytes(&[5u8; 32]);
        let pk = key.verifying_key().to_bytes().to_vec();
        let app = SignApp::new(Key(key), Trust(pk));
        let c = Challenge { nonce: vec![1, 2], context: b"ord=1".to_vec(), data_to_sign: Some(b"pay".to_vec()) };
        assert!(app.sign(&c).await.is_ok());
        // An untrusted app (different key registered) is rejected.
        let rogue = SignApp::new(Key(SigningKey::from_bytes(&[6u8; 32])), Trust(vec![0; 32]));
        assert!(rogue.sign(&c).await.is_err());
    }
}