use crate::types::TctClaims;
use crate::TctError;
use aitp_core::{base64url, Timestamp};
use aitp_crypto::{AitpVerifyingKey, Signature};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct PopChallenge {
pub tct_jti: Uuid,
pub nonce: String,
pub expires_at: Timestamp,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct PopResponse {
pub tct_jti: Uuid,
pub nonce_echo: String,
pub pop_signature: String,
}
pub fn sign_pop_response(
challenge: &PopChallenge,
holder_key: &aitp_crypto::AitpSigningKey,
) -> Result<PopResponse, TctError> {
let nonce_bytes =
base64url::decode_strict(&challenge.nonce).map_err(|_| TctError::PopFailed)?;
let digest = Sha256::digest(&nonce_bytes);
let sig = holder_key.sign(&digest);
Ok(PopResponse {
tct_jti: challenge.tct_jti,
nonce_echo: challenge.nonce.clone(),
pop_signature: sig.into_string(),
})
}
pub fn verify_pop_response(
challenge: &PopChallenge,
response: &PopResponse,
claims: &TctClaims,
now: Timestamp,
) -> Result<(), TctError> {
if response.tct_jti != challenge.tct_jti || response.tct_jti != claims.jti {
return Err(TctError::PopJtiMismatch);
}
if response.nonce_echo != challenge.nonce {
return Err(TctError::PopNonceMismatch);
}
if now.is_in_the_future(challenge.expires_at) {
return Err(TctError::PopChallengeExpired);
}
let holder_pk = AitpVerifyingKey::from_aid(&claims.sub).map_err(|_| TctError::CnfMalformed)?;
let expected_jkt = holder_pk
.to_jwk_thumbprint()
.map_err(|_| TctError::CnfMalformed)?;
if claims.cnf.jkt != expected_jkt {
return Err(TctError::CnfMalformed);
}
let nonce_bytes =
base64url::decode_strict(&challenge.nonce).map_err(|_| TctError::PopFailed)?;
let digest = Sha256::digest(&nonce_bytes);
let sig = Signature::parse(&response.pop_signature).map_err(|_| TctError::PopFailed)?;
holder_pk
.verify(&digest, &sig)
.map_err(|_| TctError::PopFailed)?;
Ok(())
}