use crate::{
cdk::types::Principal,
dto::auth::{AttestationKey, InternalInvocationProofPayloadV1, RoleAttestation},
ops::auth::{
AuthExpiryError, AuthOpsError, AuthScopeError, AuthValidationError,
InternalInvocationProofVerificationInput,
},
};
pub(super) fn verify_role_attestation_claims(
payload: &RoleAttestation,
caller: Principal,
self_pid: Principal,
verifier_subnet: Option<Principal>,
now_secs: u64,
min_accepted_epoch: u64,
) -> Result<(), AuthOpsError> {
verify_attestation_time_window(payload.issued_at, payload.expires_at, now_secs)?;
if payload.subject != caller {
return Err(AuthScopeError::AttestationSubjectMismatch {
expected: caller,
found: payload.subject,
}
.into());
}
if payload.audience != self_pid {
return Err(AuthScopeError::AttestationAudienceMismatch {
expected: self_pid,
found: payload.audience,
}
.into());
}
if let Some(attestation_subnet) = payload.subnet_id {
let verifier_subnet =
verifier_subnet.ok_or(AuthValidationError::AttestationSubnetUnavailable)?;
if attestation_subnet != verifier_subnet {
return Err(AuthScopeError::AttestationSubnetMismatch {
expected: verifier_subnet,
found: attestation_subnet,
}
.into());
}
}
if payload.epoch < min_accepted_epoch {
return Err(AuthExpiryError::AttestationEpochRejected {
epoch: payload.epoch,
min_accepted_epoch,
}
.into());
}
Ok(())
}
pub(super) fn verify_internal_invocation_proof_claims(
payload: &InternalInvocationProofPayloadV1,
input: InternalInvocationProofVerificationInput<'_>,
) -> Result<(), AuthOpsError> {
verify_attestation_time_window(payload.issued_at, payload.expires_at, input.now_secs)?;
if payload.subject != input.caller {
return Err(AuthScopeError::AttestationSubjectMismatch {
expected: input.caller,
found: payload.subject,
}
.into());
}
if payload.audience != input.self_pid {
return Err(AuthScopeError::AttestationAudienceMismatch {
expected: input.self_pid,
found: payload.audience,
}
.into());
}
if payload.audience_method != input.target_method {
return Err(AuthScopeError::InternalInvocationMethodMismatch {
expected: input.target_method.to_string(),
found: payload.audience_method.clone(),
}
.into());
}
if !input
.accepted_roles
.iter()
.any(|role| role == &payload.role)
{
return Err(AuthScopeError::InternalInvocationRoleRejected {
found: payload.role.clone(),
}
.into());
}
if let Some(attestation_subnet) = payload.subnet_id {
let verifier_subnet = input
.verifier_subnet
.ok_or(AuthValidationError::AttestationSubnetUnavailable)?;
if attestation_subnet != verifier_subnet {
return Err(AuthScopeError::AttestationSubnetMismatch {
expected: verifier_subnet,
found: attestation_subnet,
}
.into());
}
}
if payload.epoch < input.min_accepted_epoch {
return Err(AuthExpiryError::AttestationEpochRejected {
epoch: payload.epoch,
min_accepted_epoch: input.min_accepted_epoch,
}
.into());
}
Ok(())
}
fn verify_attestation_time_window(
issued_at: u64,
expires_at: u64,
now_secs: u64,
) -> Result<(), AuthOpsError> {
if expires_at <= issued_at {
return Err(AuthValidationError::AttestationInvalidWindow {
issued_at,
expires_at,
}
.into());
}
if now_secs < issued_at {
return Err(AuthExpiryError::AttestationNotYetValid {
issued_at,
now_secs,
}
.into());
}
if now_secs >= expires_at {
return Err(AuthExpiryError::AttestationExpired {
expires_at,
now_secs,
}
.into());
}
Ok(())
}
pub(super) fn verify_attestation_key_validity(
key: &AttestationKey,
now_secs: u64,
) -> Result<(), AuthOpsError> {
if let Some(valid_from) = key.valid_from
&& now_secs < valid_from
{
return Err(AuthExpiryError::AttestationKeyNotYetValid {
key_id: key.key_id,
valid_from,
now_secs,
}
.into());
}
if let Some(valid_until) = key.valid_until
&& now_secs > valid_until
{
return Err(AuthExpiryError::AttestationKeyExpired {
key_id: key.key_id,
valid_until,
now_secs,
}
.into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::verify_internal_invocation_proof_claims;
use crate::{
cdk::types::Principal,
dto::auth::{InternalInvocationProofPayloadV1, RoleAttestation},
ids::CanisterRole,
ops::auth::{
AuthExpiryError, AuthOpsError, AuthScopeError, AuthValidationError,
InternalInvocationProofVerificationInput,
},
};
fn p(id: u8) -> Principal {
Principal::from_slice(&[id; 29])
}
fn payload() -> InternalInvocationProofPayloadV1 {
InternalInvocationProofPayloadV1 {
subject: p(1),
role: CanisterRole::new("project_hub"),
subnet_id: Some(p(3)),
audience: p(2),
audience_method: "system_add_project_to_user".to_string(),
issued_at: 10,
expires_at: 20,
epoch: 4,
}
}
fn input<'a>(
target_method: &'a str,
accepted_roles: &'a [CanisterRole],
min_accepted_epoch: u64,
) -> InternalInvocationProofVerificationInput<'a> {
InternalInvocationProofVerificationInput {
caller: p(1),
self_pid: p(2),
target_method,
accepted_roles,
verifier_subnet: Some(p(3)),
now_secs: 15,
min_accepted_epoch,
}
}
fn role_attestation() -> RoleAttestation {
RoleAttestation {
subject: p(1),
role: CanisterRole::new("project_hub"),
subnet_id: Some(p(3)),
audience: p(2),
issued_at: 10,
expires_at: 20,
epoch: 4,
}
}
#[test]
fn internal_invocation_claims_accept_bound_method_role_and_subnet() {
let accepted = [CanisterRole::new("project_hub")];
verify_internal_invocation_proof_claims(
&payload(),
input("system_add_project_to_user", &accepted, 4),
)
.expect("valid internal proof claims");
}
#[test]
fn internal_invocation_claims_reject_method_mismatch() {
let accepted = [CanisterRole::new("project_hub")];
let err = verify_internal_invocation_proof_claims(
&payload(),
input("other_method", &accepted, 4),
)
.expect_err("method mismatch must reject");
std::assert_matches!(
err,
AuthOpsError::Scope(AuthScopeError::InternalInvocationMethodMismatch { .. })
);
}
#[test]
fn internal_invocation_claims_reject_role_mismatch() {
let accepted = [CanisterRole::new("admin_hub")];
let err = verify_internal_invocation_proof_claims(
&payload(),
input("system_add_project_to_user", &accepted, 4),
)
.expect_err("role mismatch must reject");
std::assert_matches!(
err,
AuthOpsError::Scope(AuthScopeError::InternalInvocationRoleRejected { .. })
);
}
#[test]
fn internal_invocation_claims_reject_stale_epoch() {
let accepted = [CanisterRole::new("project_hub")];
let err = verify_internal_invocation_proof_claims(
&payload(),
input("system_add_project_to_user", &accepted, 5),
)
.expect_err("stale epoch must reject");
std::assert_matches!(
err,
AuthOpsError::Expiry(AuthExpiryError::AttestationEpochRejected { .. })
);
}
#[test]
fn internal_invocation_claims_reject_future_issued_at() {
let accepted = [CanisterRole::new("project_hub")];
let mut payload = payload();
payload.issued_at = 16;
payload.expires_at = 30;
let err = verify_internal_invocation_proof_claims(
&payload,
input("system_add_project_to_user", &accepted, 4),
)
.expect_err("future issued_at must reject");
std::assert_matches!(
err,
AuthOpsError::Expiry(AuthExpiryError::AttestationNotYetValid { .. })
);
}
#[test]
fn internal_invocation_claims_reject_invalid_time_window() {
let accepted = [CanisterRole::new("project_hub")];
let mut payload = payload();
payload.expires_at = payload.issued_at;
let err = verify_internal_invocation_proof_claims(
&payload,
input("system_add_project_to_user", &accepted, 4),
)
.expect_err("invalid attestation time window must reject");
std::assert_matches!(
err,
AuthOpsError::Validation(AuthValidationError::AttestationInvalidWindow { .. })
);
}
#[test]
fn internal_invocation_claims_reject_expiry_boundary() {
let accepted = [CanisterRole::new("project_hub")];
let mut payload = payload();
payload.issued_at = 10;
payload.expires_at = 15;
let err = verify_internal_invocation_proof_claims(
&payload,
input("system_add_project_to_user", &accepted, 4),
)
.expect_err("attestation at expiry boundary must reject");
std::assert_matches!(
err,
AuthOpsError::Expiry(AuthExpiryError::AttestationExpired { .. })
);
}
#[test]
fn role_attestation_claims_reject_future_issued_at() {
let mut payload = role_attestation();
payload.issued_at = 16;
payload.expires_at = 30;
let err = super::verify_role_attestation_claims(&payload, p(1), p(2), Some(p(3)), 15, 4)
.expect_err("future issued_at must reject");
std::assert_matches!(
err,
AuthOpsError::Expiry(AuthExpiryError::AttestationNotYetValid { .. })
);
}
#[test]
fn role_attestation_claims_reject_invalid_time_window() {
let mut payload = role_attestation();
payload.expires_at = payload.issued_at;
let err = super::verify_role_attestation_claims(&payload, p(1), p(2), Some(p(3)), 15, 4)
.expect_err("invalid attestation time window must reject");
std::assert_matches!(
err,
AuthOpsError::Validation(AuthValidationError::AttestationInvalidWindow { .. })
);
}
#[test]
fn role_attestation_claims_reject_expiry_boundary() {
let mut payload = role_attestation();
payload.issued_at = 10;
payload.expires_at = 15;
let err = super::verify_role_attestation_claims(&payload, p(1), p(2), Some(p(3)), 15, 4)
.expect_err("attestation at expiry boundary must reject");
std::assert_matches!(
err,
AuthOpsError::Expiry(AuthExpiryError::AttestationExpired { .. })
);
}
}