use super::dependency_unavailable;
use crate::{
InternalError,
access::AccessError,
cdk::{
candid::de::{DecoderConfig, IDLDeserialize},
types::Principal,
},
diagnostics::codes,
dto::auth::DelegatedToken,
ops::{
auth::{AuthOps, VerifyDelegatedTokenRuntimeInput},
ic::IcOps,
},
};
use ic_cdk::api::msg_arg_data;
const DELEGATED_TOKEN_DECODING_QUOTA: usize = 256 * 1024;
const DELEGATED_TOKEN_MAX_TYPE_LEN: usize = 16 * 1024;
pub(super) fn delegated_token_verified(
authenticated_subject: Principal,
required_scope: Option<&str>,
) -> Result<Principal, AccessError> {
let token = delegated_token_from_args()?;
let now_ns = IcOps::now_nanos();
verify_token(token, authenticated_subject, now_ns, required_scope)
}
fn verify_token(
token: DelegatedToken,
caller: Principal,
now_ns: u64,
required_scope: Option<&str>,
) -> Result<Principal, AccessError> {
let max_ttl_ns = delegated_token_max_ttl_ns()?;
let required_scopes = required_scope
.map(|scope| vec![scope.to_string()])
.unwrap_or_default();
let verified = AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
token: &token,
caller,
max_cert_ttl_ns: max_ttl_ns,
max_token_ttl_ns: max_ttl_ns,
required_scopes: &required_scopes,
now_ns,
})
.map_err(access_error_from_verification)?;
Ok(verified.issuer())
}
fn access_error_from_verification(err: InternalError) -> AccessError {
match err.public_error().code() {
code if code == codes::AUTH_CERT_EXPIRED.raw_code() => {
AccessError::DelegatedAuthCertExpired
}
code if code == codes::AUTH_TOKEN_EXPIRED.raw_code() => {
AccessError::DelegatedAuthTokenExpired
}
_ => AccessError::Internal(err),
}
}
fn delegated_token_from_args() -> Result<DelegatedToken, AccessError> {
let bytes = msg_arg_data();
delegated_token_from_ingress_bytes(&bytes)
}
fn delegated_token_from_ingress_bytes(bytes: &[u8]) -> Result<DelegatedToken, AccessError> {
delegated_token_from_bytes(bytes).map_err(|_| AccessError::DelegatedTokenMalformed)
}
fn delegated_token_from_bytes(bytes: &[u8]) -> Result<DelegatedToken, String> {
let mut config = DecoderConfig::new();
config
.set_decoding_quota(DELEGATED_TOKEN_DECODING_QUOTA)
.set_max_type_len(DELEGATED_TOKEN_MAX_TYPE_LEN)
.set_full_error_message(false);
let mut decoder = IDLDeserialize::new_with_config(bytes, &config)
.map_err(|err| format!("failed to decode ingress arguments: {err}"))?;
decoder
.get_value::<DelegatedToken>()
.map_err(|err| err.to_string())
}
fn delegated_token_max_ttl_ns() -> Result<u64, AccessError> {
AuthOps::delegated_token_max_ttl_ns().map_err(dependency_unavailable)
}
#[cfg(test)]
mod tests {
use super::{access_error_from_verification, delegated_token_from_ingress_bytes};
use crate::{
InternalError,
access::AccessError,
cdk::{
candid::{Principal, encode_args},
types,
},
dto::auth::{
DelegatedRoleGrant, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
DelegationCert, DelegationProof, IcCanisterSignatureProofV1, IssuerProof,
IssuerProofAlgorithm, IssuerProofBinding,
},
};
#[test]
fn verification_expiry_codes_map_to_typed_access_denials() {
let token = access_error_from_verification(InternalError::auth_token_expired());
assert!(matches!(token, AccessError::DelegatedAuthTokenExpired));
let cert = access_error_from_verification(InternalError::auth_proof_expired());
assert!(matches!(cert, AccessError::DelegatedAuthCertExpired));
let other = access_error_from_verification(InternalError::state_failure());
assert!(matches!(other, AccessError::Internal(_)));
}
#[test]
fn delegated_token_decode_allows_large_trailing_endpoint_payload() {
let token = token_with_scopes(vec!["upload:image".to_string()]);
let chunk = vec![7_u8; 128 * 1024];
let bytes = encode_args((token.clone(), chunk)).expect("encode auth call");
let decoded =
delegated_token_from_ingress_bytes(&bytes).expect("large trailing payload must pass");
assert_eq!(decoded, token);
}
#[test]
fn delegated_token_decode_rejects_oversized_token() {
let token = token_with_scopes(vec!["x".repeat(300 * 1024)]);
let bytes = encode_args((token, Vec::<u8>::new())).expect("encode auth call");
let err =
delegated_token_from_ingress_bytes(&bytes).expect_err("oversized token must fail");
assert!(matches!(
err,
crate::access::AccessError::DelegatedTokenMalformed
));
}
#[test]
fn delegated_auth_guard_has_no_verifier_local_use_store() {
let source = include_str!("token.rs");
let storage_fn = ["consume_delegated", "_token_use"].concat();
let access_fn = ["consume_update", "_token_once"].concat();
assert!(!source.contains(&storage_fn));
assert!(!source.contains(&access_fn));
}
#[test]
fn delegated_auth_guard_delegates_presenter_and_scope_policy_once() {
let source = include_str!("token.rs");
let start = source
.find("fn verify_token(")
.expect("verify_token exists");
let end = source[start..]
.find("fn access_error_from_verification")
.map_or(source.len(), |offset| start + offset);
let body = &source[start..end];
assert_eq!(body.matches("AuthOps::verify_token").count(), 1);
assert!(!body.contains("verified.scopes().contains"));
}
#[test]
fn delegated_auth_guard_uses_nanosecond_now_for_verification() {
let source = include_str!("token.rs");
let start = source
.find("pub(super) fn delegated_token_verified")
.expect("delegated_token_verified exists");
let end = source[start..]
.find("// Verify a delegated token")
.map_or(source.len(), |offset| start + offset);
let body = &source[start..end];
assert!(body.contains("IcOps::now_nanos()"));
assert!(!body.contains("IcOps::now_secs()"));
}
fn token_with_scopes(scopes: Vec<String>) -> DelegatedToken {
let issuer_proof_alg = IssuerProofAlgorithm::IcCanisterSignatureV1;
let issuer_proof_binding = IssuerProofBinding::IcCanisterSignatureV1 {
seed_hash: [10; 32],
};
DelegatedToken {
claims: DelegatedTokenClaims {
presenter: p(1),
subject: p(1),
issuer_pid: p(2),
cert_hash: [3; 32],
issued_at_ns: 10,
expires_at_ns: 20,
aud: DelegationAudience::Fleet(crate::test::support::fleet_key(1)),
grants: vec![grant("project_instance", &scopes)],
nonce: [5; 16],
ext: None,
},
proof: DelegationProof {
cert: DelegationCert {
root_pid: p(6),
issuer_pid: p(2),
issuer_proof_alg,
issuer_proof_binding_hash: [11; 32],
issuer_proof_binding,
issued_at_ns: 10,
not_before_ns: 10,
expires_at_ns: 20,
max_token_ttl_ns: 10,
aud: DelegationAudience::Fleet(crate::test::support::fleet_key(1)),
grants: vec![grant("project_instance", &scopes)],
},
root_proof: crate::ops::auth::test_fixtures::chain_key_root_proof(12),
},
issuer_proof: IssuerProof::IcCanisterSignatureV1(IcCanisterSignatureProofV1 {
signature_cbor: vec![13; 64],
public_key_der: vec![14; 32],
}),
}
}
fn grant(role: &str, scopes: &[String]) -> DelegatedRoleGrant {
DelegatedRoleGrant {
target: crate::ids::CanisterRole::owned(role.to_string()),
scopes: scopes.to_vec(),
}
}
fn p(id: u8) -> Principal {
types::Principal::from_slice(&[id; 29])
}
}