use auths_core::storage::keychain::{KeyAlias, sign_with_key};
use auths_verifier::{PresentationBinding, PresentationEnvelope};
use chrono::{DateTime, Utc};
use ring::rand::SecureRandom;
use crate::context::AuthsContext;
use crate::domains::credentials::error::CredentialError;
const NONCE_LEN: usize = 32;
#[derive(Debug, Clone)]
pub struct ChallengeSession {
nonce: Vec<u8>,
audience: String,
credential_said: String,
consumed: bool,
}
impl ChallengeSession {
pub fn issue(audience: &str, credential_said: &str) -> Result<Self, CredentialError> {
let mut nonce = vec![0u8; NONCE_LEN];
ring::rand::SystemRandom::new()
.fill(&mut nonce)
.map_err(|_| CredentialError::SchemaUnknown)?;
Ok(Self {
nonce,
audience: audience.to_string(),
credential_said: credential_said.to_string(),
consumed: false,
})
}
pub fn nonce(&self) -> &[u8] {
&self.nonce
}
pub fn audience(&self) -> &str {
&self.audience
}
pub fn credential_said(&self) -> &str {
&self.credential_said
}
pub fn consume(&mut self) -> Option<Vec<u8>> {
if self.consumed {
return None;
}
self.consumed = true;
Some(self.nonce.clone())
}
pub fn is_consumed(&self) -> bool {
self.consumed
}
}
#[derive(Debug, Clone)]
pub enum PresentationChallenge {
Challenge {
nonce: Vec<u8>,
},
Ttl {
not_after: DateTime<Utc>,
},
}
pub fn present_credential(
ctx: &AuthsContext,
subject_alias: &KeyAlias,
credential_said: &str,
audience: &str,
challenge: PresentationChallenge,
) -> Result<PresentationEnvelope, CredentialError> {
let binding = match challenge {
PresentationChallenge::Challenge { nonce } => PresentationBinding::Challenge { nonce },
PresentationChallenge::Ttl { not_after } => {
let mut nonce = vec![0u8; NONCE_LEN];
ring::rand::SystemRandom::new()
.fill(&mut nonce)
.map_err(|_| CredentialError::SchemaUnknown)?;
PresentationBinding::Ttl { nonce, not_after }
}
};
let message = signed_message(credential_said, audience, binding_nonce(&binding));
let (signature, _pk, _curve) = sign_with_key(
ctx.key_storage.as_ref(),
subject_alias,
ctx.passphrase_provider.as_ref(),
&message,
)?;
Ok(PresentationEnvelope {
credential_said: credential_said.to_string(),
audience: audience.to_string(),
binding,
signature,
})
}
fn binding_nonce(binding: &PresentationBinding) -> &[u8] {
match binding {
PresentationBinding::Challenge { nonce } => nonce,
PresentationBinding::Ttl { nonce, .. } => nonce,
}
}
fn signed_message(credential_said: &str, audience: &str, nonce: &[u8]) -> Vec<u8> {
let mut message = Vec::with_capacity(credential_said.len() + audience.len() + nonce.len() + 2);
message.extend_from_slice(credential_said.as_bytes());
message.push(0);
message.extend_from_slice(audience.as_bytes());
message.push(0);
message.extend_from_slice(nonce);
message
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn challenge_session_consumes_once() {
let mut session = ChallengeSession::issue("aud", "ECred").expect("issue challenge session");
assert!(!session.is_consumed());
let first = session.consume();
assert!(first.is_some(), "first consume yields the nonce");
assert!(session.is_consumed());
assert!(
session.consume().is_none(),
"a consumed challenge yields nothing (one-shot)"
);
}
#[test]
fn issued_nonce_is_full_length_and_random() {
let a = ChallengeSession::issue("aud", "ECred").unwrap();
let b = ChallengeSession::issue("aud", "ECred").unwrap();
assert_eq!(a.nonce().len(), NONCE_LEN);
assert_ne!(a.nonce(), b.nonce(), "nonces must differ across sessions");
}
}