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());
}
}