use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use auths_keri::witness::StoredReceipt;
use auths_keri::{Acdc, Event, TelEvent};
use crate::commit_kel::VerifierWitnessPolicy;
use crate::credential::{CredentialVerdict, LifecycleEvent, SignedAcdc, verify_credential_sync};
use crate::presentation::{
PresentationBinding, PresentationEnvelope, PresentationVerdict, verify_presentation_sync,
};
pub const SCHEMA_VERSION: u32 = 1;
const MAX_REQUEST_BYTES: usize = 1024 * 1024;
const MAX_KEL_EVENTS: usize = 1024;
const MAX_TEL_EVENTS: usize = 1024;
const MAX_RECEIPTS: usize = 4096;
const SERIALIZE_FALLBACK: &str = "{\"schemaVersion\":1,\"kind\":\"malformedRequest\",\"message\":\"verdict serialization failed\"}";
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct WireEnvelope<V> {
schema_version: u32,
#[serde(flatten)]
verdict: V,
}
#[derive(Serialize)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
enum WirePresentationVerdict {
Valid {
issuer: String,
subject: String,
caps: Vec<String>,
role: Option<String>,
expires_at: Option<String>,
},
HolderNotCurrentKey,
WrongAudience,
NonceMismatchOrConsumed,
Expired,
SubjectKelInvalid,
CredentialNotValid {
credential: WireCredentialVerdict,
},
MalformedRequest {
message: String,
},
InputTooLarge {
field: String,
count: usize,
limit: usize,
},
UnsupportedSchemaVersion {
got: u32,
expected: u32,
},
}
#[derive(Serialize)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
enum WireCredentialVerdict {
Valid {
issuer: String,
subject: String,
caps: Vec<String>,
as_of: u128,
},
SaidMismatch,
SchemaInvalid,
IssuerSignatureInvalid,
RegistryNotEstablished,
CredentialRevoked {
revoked_at: u128,
},
Expired {
expired_at: String,
now: String,
},
WitnessQuorumNotMet {
event: String,
collected: usize,
required: usize,
},
IssuerKelDuplicitous,
MalformedRequest {
message: String,
},
InputTooLarge {
field: String,
count: usize,
limit: usize,
},
UnsupportedSchemaVersion {
got: u32,
expected: u32,
},
}
impl From<PresentationVerdict> for WirePresentationVerdict {
fn from(verdict: PresentationVerdict) -> Self {
match verdict {
PresentationVerdict::Valid {
issuer,
subject,
caps,
role,
expires_at,
} => WirePresentationVerdict::Valid {
issuer: issuer.as_str().to_string(),
subject: subject.as_str().to_string(),
caps: caps.iter().map(|c| c.as_str().to_string()).collect(),
role,
expires_at: expires_at.map(|t| t.to_rfc3339()),
},
PresentationVerdict::HolderNotCurrentKey => {
WirePresentationVerdict::HolderNotCurrentKey
}
PresentationVerdict::WrongAudience => WirePresentationVerdict::WrongAudience,
PresentationVerdict::NonceMismatchOrConsumed => {
WirePresentationVerdict::NonceMismatchOrConsumed
}
PresentationVerdict::Expired => WirePresentationVerdict::Expired,
PresentationVerdict::SubjectKelInvalid => WirePresentationVerdict::SubjectKelInvalid,
PresentationVerdict::CredentialNotValid(inner) => {
WirePresentationVerdict::CredentialNotValid {
credential: inner.into(),
}
}
}
}
}
impl From<CredentialVerdict> for WireCredentialVerdict {
fn from(verdict: CredentialVerdict) -> Self {
match verdict {
CredentialVerdict::Valid {
issuer,
subject,
caps,
as_of,
} => WireCredentialVerdict::Valid {
issuer: issuer.as_str().to_string(),
subject: subject.as_str().to_string(),
caps: caps.iter().map(|c| c.as_str().to_string()).collect(),
as_of,
},
CredentialVerdict::SaidMismatch => WireCredentialVerdict::SaidMismatch,
CredentialVerdict::SchemaInvalid => WireCredentialVerdict::SchemaInvalid,
CredentialVerdict::IssuerSignatureInvalid => {
WireCredentialVerdict::IssuerSignatureInvalid
}
CredentialVerdict::RegistryNotEstablished => {
WireCredentialVerdict::RegistryNotEstablished
}
CredentialVerdict::CredentialRevoked { revoked_at } => {
WireCredentialVerdict::CredentialRevoked { revoked_at }
}
CredentialVerdict::Expired { expired_at, now } => WireCredentialVerdict::Expired {
expired_at: expired_at.to_rfc3339(),
now: now.to_rfc3339(),
},
CredentialVerdict::WitnessQuorumNotMet {
event,
collected,
required,
} => WireCredentialVerdict::WitnessQuorumNotMet {
event: lifecycle_tag(event).to_string(),
collected,
required,
},
CredentialVerdict::IssuerKelDuplicitous => WireCredentialVerdict::IssuerKelDuplicitous,
}
}
}
fn lifecycle_tag(event: LifecycleEvent) -> &'static str {
match event {
LifecycleEvent::Vcp => "vcp",
LifecycleEvent::Iss => "iss",
LifecycleEvent::Rev => "rev",
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct VerifyPresentationRequest {
#[serde(rename = "schemaVersion")]
_schema_version: u32,
envelope: WireEnvelopeIn,
credential: WireSignedAcdc,
issuer_kel: Vec<Event>,
subject_kel: Vec<Event>,
#[serde(default)]
delegator_kel: Vec<Event>,
tel: Vec<TelEvent>,
#[serde(default)]
receipts: Vec<StoredReceipt>,
witness_policy: WireWitnessPolicy,
audience: String,
#[serde(default)]
expected_challenge_b64: Option<String>,
now: DateTime<Utc>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct VerifyCredentialRequest {
#[serde(rename = "schemaVersion")]
_schema_version: u32,
credential: WireSignedAcdc,
issuer_kel: Vec<Event>,
tel: Vec<TelEvent>,
#[serde(default)]
receipts: Vec<StoredReceipt>,
witness_policy: WireWitnessPolicy,
now: DateTime<Utc>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireEnvelopeIn {
credential_said: String,
audience: String,
binding: WireBindingIn,
signature_b64: String,
}
#[derive(Deserialize)]
#[serde(tag = "mode", rename_all = "camelCase")]
enum WireBindingIn {
#[serde(rename_all = "camelCase")]
Challenge { nonce_b64: String },
#[serde(rename_all = "camelCase")]
Ttl {
nonce_b64: String,
not_after: DateTime<Utc>,
},
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireSignedAcdc {
acdc: Acdc,
signature_b64: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct VersionPeek {
schema_version: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
enum WireWitnessPolicy {
Warn,
RequireWitnesses,
}
impl From<WireWitnessPolicy> for VerifierWitnessPolicy {
fn from(policy: WireWitnessPolicy) -> Self {
match policy {
WireWitnessPolicy::Warn => VerifierWitnessPolicy::Warn,
WireWitnessPolicy::RequireWitnesses => VerifierWitnessPolicy::RequireWitnesses,
}
}
}
enum RequestError {
Malformed(String),
TooLarge {
field: &'static str,
count: usize,
limit: usize,
},
UnsupportedSchemaVersion(u32),
}
impl From<RequestError> for WirePresentationVerdict {
fn from(error: RequestError) -> Self {
match error {
RequestError::Malformed(message) => {
WirePresentationVerdict::MalformedRequest { message }
}
RequestError::TooLarge {
field,
count,
limit,
} => WirePresentationVerdict::InputTooLarge {
field: field.to_string(),
count,
limit,
},
RequestError::UnsupportedSchemaVersion(got) => {
WirePresentationVerdict::UnsupportedSchemaVersion {
got,
expected: SCHEMA_VERSION,
}
}
}
}
}
impl From<RequestError> for WireCredentialVerdict {
fn from(error: RequestError) -> Self {
match error {
RequestError::Malformed(message) => WireCredentialVerdict::MalformedRequest { message },
RequestError::TooLarge {
field,
count,
limit,
} => WireCredentialVerdict::InputTooLarge {
field: field.to_string(),
count,
limit,
},
RequestError::UnsupportedSchemaVersion(got) => {
WireCredentialVerdict::UnsupportedSchemaVersion {
got,
expected: SCHEMA_VERSION,
}
}
}
}
}
pub fn verify_presentation_json(request_json: &str) -> String {
let verdict = run_presentation(request_json).unwrap_or_else(WirePresentationVerdict::from);
serialize_verdict(verdict)
}
pub fn verify_credential_json(request_json: &str) -> String {
let verdict = run_credential(request_json).unwrap_or_else(WireCredentialVerdict::from);
serialize_verdict(verdict)
}
fn serialize_verdict<V: Serialize>(verdict: V) -> String {
serde_json::to_string(&WireEnvelope {
schema_version: SCHEMA_VERSION,
verdict,
})
.unwrap_or_else(|_| SERIALIZE_FALLBACK.to_string())
}
fn run_presentation(request_json: &str) -> Result<WirePresentationVerdict, RequestError> {
check_request_size(request_json)?;
check_schema_version(request_json)?;
let request: VerifyPresentationRequest =
serde_json::from_str(request_json).map_err(|e| RequestError::Malformed(e.to_string()))?;
check_bound("issuerKel", request.issuer_kel.len(), MAX_KEL_EVENTS)?;
check_bound("subjectKel", request.subject_kel.len(), MAX_KEL_EVENTS)?;
check_bound("delegatorKel", request.delegator_kel.len(), MAX_KEL_EVENTS)?;
check_bound("tel", request.tel.len(), MAX_TEL_EVENTS)?;
check_bound("receipts", request.receipts.len(), MAX_RECEIPTS)?;
let signature = decode_b64("credential.signatureB64", &request.credential.signature_b64)?;
let signed = SignedAcdc {
acdc: request.credential.acdc,
signature,
};
let envelope = build_envelope(request.envelope)?;
let expected_challenge = match &request.expected_challenge_b64 {
Some(nonce) => Some(decode_b64("expectedChallengeB64", nonce)?),
None => None,
};
Ok(verify_presentation_sync(
&envelope,
&signed,
&request.issuer_kel,
&request.tel,
&request.receipts,
request.witness_policy.into(),
&request.subject_kel,
&request.delegator_kel,
&request.audience,
expected_challenge.as_deref(),
request.now,
)
.into())
}
fn run_credential(request_json: &str) -> Result<WireCredentialVerdict, RequestError> {
check_request_size(request_json)?;
check_schema_version(request_json)?;
let request: VerifyCredentialRequest =
serde_json::from_str(request_json).map_err(|e| RequestError::Malformed(e.to_string()))?;
check_bound("issuerKel", request.issuer_kel.len(), MAX_KEL_EVENTS)?;
check_bound("tel", request.tel.len(), MAX_TEL_EVENTS)?;
check_bound("receipts", request.receipts.len(), MAX_RECEIPTS)?;
let signature = decode_b64("credential.signatureB64", &request.credential.signature_b64)?;
let signed = SignedAcdc {
acdc: request.credential.acdc,
signature,
};
Ok(verify_credential_sync(
&signed,
&request.issuer_kel,
&request.tel,
&request.receipts,
request.witness_policy.into(),
request.now,
)
.into())
}
fn build_envelope(wire: WireEnvelopeIn) -> Result<PresentationEnvelope, RequestError> {
let signature = decode_b64("envelope.signatureB64", &wire.signature_b64)?;
let binding = match wire.binding {
WireBindingIn::Challenge { nonce_b64 } => PresentationBinding::Challenge {
nonce: decode_b64("envelope.binding.nonceB64", &nonce_b64)?,
},
WireBindingIn::Ttl {
nonce_b64,
not_after,
} => PresentationBinding::Ttl {
nonce: decode_b64("envelope.binding.nonceB64", &nonce_b64)?,
not_after,
},
};
Ok(PresentationEnvelope {
credential_said: wire.credential_said,
audience: wire.audience,
binding,
signature,
})
}
fn check_request_size(request_json: &str) -> Result<(), RequestError> {
if request_json.len() > MAX_REQUEST_BYTES {
return Err(RequestError::TooLarge {
field: "request",
count: request_json.len(),
limit: MAX_REQUEST_BYTES,
});
}
Ok(())
}
fn check_schema_version(request_json: &str) -> Result<(), RequestError> {
let peek: VersionPeek =
serde_json::from_str(request_json).map_err(|e| RequestError::Malformed(e.to_string()))?;
if peek.schema_version != SCHEMA_VERSION {
return Err(RequestError::UnsupportedSchemaVersion(peek.schema_version));
}
Ok(())
}
fn check_bound(field: &'static str, count: usize, limit: usize) -> Result<(), RequestError> {
if count > limit {
return Err(RequestError::TooLarge {
field,
count,
limit,
});
}
Ok(())
}
fn decode_b64(field: &str, value: &str) -> Result<Vec<u8>, RequestError> {
BASE64
.decode(value)
.map_err(|e| RequestError::Malformed(format!("{field}: invalid base64 ({e})")))
}