use super::*;
use crate::{
dto::{
auth::RoleAttestationRequest,
capability::{
CAPABILITY_VERSION_V1, CapabilityProof, CapabilityProofBlob, DelegatedGrant,
DelegatedGrantProof, DelegatedGrantScope, PROOF_VERSION_V1,
},
error::ErrorCode,
rpc::{CyclesRequest, RootRequestMetadata},
},
ops::storage::state::subnet::SubnetStateOps,
};
#[cfg(feature = "auth-shard-secp256k1-verify")]
use k256::ecdsa::{Signature, SigningKey, signature::hazmat::PrehashSigner};
const NS_PER_SEC: u64 = 1_000_000_000;
fn p(id: u8) -> Principal {
Principal::from_slice(&[id; 29])
}
fn sample_request(cycles: u128) -> Request {
Request::Cycles(CyclesRequest {
cycles,
metadata: None,
})
}
fn sample_metadata(
request_id: u8,
nonce: u8,
issued_at_ns: u64,
ttl_ns: u64,
) -> CapabilityRequestMetadata {
CapabilityRequestMetadata {
request_id: [request_id; 16],
nonce: [nonce; 16],
issued_at_ns,
ttl_ns,
}
}
#[test]
fn root_capability_hash_changes_with_payload() {
let hash_a =
root_capability_hash(p(1), CAPABILITY_VERSION_V1, &sample_request(10)).expect("hash a");
let hash_b =
root_capability_hash(p(1), CAPABILITY_VERSION_V1, &sample_request(11)).expect("hash b");
assert_ne!(hash_a, hash_b);
}
#[test]
fn root_capability_hash_binds_target_canister() {
let req = sample_request(10);
let hash_a = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &req).expect("hash a");
let hash_b = root_capability_hash(p(2), CAPABILITY_VERSION_V1, &req).expect("hash b");
assert_ne!(hash_a, hash_b);
}
#[test]
fn root_capability_hash_binds_capability_version() {
let req = sample_request(10);
let hash_a = root_capability_hash(p(1), 1, &req).expect("hash a");
let hash_b = root_capability_hash(p(1), 2, &req).expect("hash b");
assert_ne!(hash_a, hash_b);
}
#[test]
fn root_capability_hash_ignores_request_metadata() {
let req_a = Request::Cycles(CyclesRequest {
cycles: 10,
metadata: Some(RootRequestMetadata {
request_id: [1u8; 32],
ttl_ns: 60 * NS_PER_SEC,
}),
});
let req_b = Request::Cycles(CyclesRequest {
cycles: 10,
metadata: Some(RootRequestMetadata {
request_id: [2u8; 32],
ttl_ns: 120 * NS_PER_SEC,
}),
});
let hash_a = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &req_a).expect("hash a");
let hash_b = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &req_b).expect("hash b");
assert_eq!(hash_a, hash_b);
}
#[test]
fn root_capability_hash_ignores_role_attestation_request_epoch() {
let request = |epoch| {
Request::IssueRoleAttestation(RoleAttestationRequest {
subject: p(1),
role: crate::ids::CanisterRole::new("project_hub"),
subnet_id: Some(p(2)),
audience: p(3),
ttl_ns: 60 * NS_PER_SEC,
epoch,
metadata: Some(RootRequestMetadata {
request_id: [4u8; 32],
ttl_ns: 60 * NS_PER_SEC,
}),
})
};
let hash_a = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &request(0)).expect("hash a");
let hash_b = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &request(9)).expect("hash b");
assert_eq!(
hash_a, hash_b,
"root capability proof binding must ignore caller-supplied role-attestation epoch"
);
}
#[test]
fn project_replay_metadata_rejects_expired_metadata() {
let err = project_replay_metadata(
sample_metadata(1, 2, 900 * NS_PER_SEC, 50 * NS_PER_SEC),
1_000 * NS_PER_SEC,
)
.expect_err("expired metadata must fail");
assert_eq!(err.code, ErrorCode::Conflict);
}
#[test]
fn project_replay_metadata_rejects_expiry_boundary() {
let err = project_replay_metadata(
sample_metadata(1, 2, 900 * NS_PER_SEC, 50 * NS_PER_SEC),
950 * NS_PER_SEC,
)
.expect_err("metadata at expiry boundary must fail");
assert_eq!(err.code, ErrorCode::Conflict);
}
#[test]
fn project_replay_metadata_rejects_future_metadata_beyond_skew() {
let err = project_replay_metadata(
sample_metadata(1, 2, 1_031 * NS_PER_SEC, 60 * NS_PER_SEC),
1_000 * NS_PER_SEC,
)
.expect_err("future metadata must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn project_replay_metadata_binds_nonce_into_request_id() {
let a = project_replay_metadata(
sample_metadata(3, 1, 1_000 * NS_PER_SEC, 60 * NS_PER_SEC),
1_000 * NS_PER_SEC,
)
.expect("a");
let b = project_replay_metadata(
sample_metadata(3, 2, 1_000 * NS_PER_SEC, 60 * NS_PER_SEC),
1_000 * NS_PER_SEC,
)
.expect("b");
assert_ne!(a.request_id, b.request_id);
}
#[test]
fn with_root_request_metadata_overrides_existing_metadata() {
let request = Request::Cycles(CyclesRequest {
cycles: 10,
metadata: Some(RootRequestMetadata {
request_id: [7u8; 32],
ttl_ns: 10 * NS_PER_SEC,
}),
});
let metadata = RootRequestMetadata {
request_id: [9u8; 32],
ttl_ns: 60 * NS_PER_SEC,
};
let updated = with_root_request_metadata(request, metadata);
match updated {
Request::Cycles(req) => assert_eq!(req.metadata, Some(metadata)),
_ => panic!("expected cycles request"),
}
}
fn sample_delegated_grant_proof(
capability: &Request,
caller: Principal,
target_canister: Principal,
now_ns: u64,
) -> DelegatedGrantProof {
let capability_hash =
root_capability_hash(target_canister, CAPABILITY_VERSION_V1, capability).expect("hash");
DelegatedGrantProof {
proof_version: PROOF_VERSION_V1,
capability_hash,
grant: DelegatedGrant {
issuer: target_canister,
subject: caller,
audience: vec![target_canister],
scope: DelegatedGrantScope {
service: CapabilityService::Root,
capability_family: root_capability_family(capability).to_string(),
},
capability_hash,
quota: 1,
issued_at_ns: now_ns.saturating_sub(10 * NS_PER_SEC),
expires_at_ns: now_ns.saturating_add(10 * NS_PER_SEC),
epoch: 0,
},
grant_sig: vec![1],
key_id: DELEGATED_GRANT_KEY_ID_V1,
}
}
fn role_attestation_capability_proof(proof_version: u16) -> CapabilityProof {
CapabilityProof::RoleAttestation(CapabilityProofBlob {
proof_version,
capability_hash: [0u8; 32],
payload: Vec::new(),
})
}
fn delegated_grant_capability_proof(proof: DelegatedGrantProof) -> CapabilityProof {
proof
.try_into()
.expect("delegated grant proof should encode")
}
#[cfg(feature = "auth-shard-secp256k1-verify")]
fn sign_delegated_grant(seed: u8, grant: &DelegatedGrant) -> (Vec<u8>, Vec<u8>) {
let signing_key = SigningKey::from_bytes((&[seed; 32]).into()).expect("signing key");
let signature: Signature = signing_key
.sign_prehash(&delegated_grant_hash(grant).expect("hash"))
.expect("prehash signature");
let public_key = signing_key
.verifying_key()
.to_encoded_point(true)
.as_bytes()
.to_vec();
(public_key, signature.to_bytes().to_vec())
}
#[test]
fn delegated_grant_blob_rejects_header_mismatch() {
let request = sample_request(10);
let proof = sample_delegated_grant_proof(&request, p(2), p(1), 100 * NS_PER_SEC);
let mut blob = super::proof::encode_delegated_grant_blob(&proof).expect("encode blob");
blob.capability_hash = [9u8; 32];
let err =
super::proof::decode_delegated_grant_blob(&blob).expect_err("header mismatch must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn validate_nonroot_cycles_envelope_accepts_structural_cycles() {
validate_nonroot_cycles_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&CapabilityProof::Structural,
)
.expect("structural cycles envelope must be accepted for non-root path");
}
#[test]
fn validate_nonroot_cycles_envelope_rejects_non_structural_proof() {
let err = validate_nonroot_cycles_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&role_attestation_capability_proof(PROOF_VERSION_V1),
)
.expect_err("non-root path must reject non-structural proof");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn validate_root_capability_envelope_rejects_capability_version_mismatch() {
let err = validate_root_capability_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1 + 1,
&CapabilityProof::Structural,
)
.expect_err("unsupported capability version must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn validate_root_capability_envelope_returns_structural_mode() {
let proof = validate_root_capability_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&CapabilityProof::Structural,
)
.expect("valid structural proof must validate");
assert_eq!(proof.mode(), RootCapabilityProofMode::Structural);
}
#[test]
fn validate_root_capability_envelope_returns_delegated_grant_mode() {
let request = sample_request(10);
let proof = sample_delegated_grant_proof(&request, p(2), p(1), 100 * NS_PER_SEC);
let capability_proof = delegated_grant_capability_proof(proof);
let proof = validate_root_capability_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&capability_proof,
)
.expect("valid delegated grant proof header must validate");
assert_eq!(proof.mode(), RootCapabilityProofMode::DelegatedGrant);
}
#[test]
fn validate_nonroot_cycles_envelope_returns_structural_mode() {
let proof = validate_nonroot_cycles_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&CapabilityProof::Structural,
)
.expect("valid non-root structural proof must validate");
assert_eq!(proof.mode(), RootCapabilityProofMode::Structural);
}
#[test]
fn validate_root_capability_envelope_rejects_role_attestation_proof_after_hard_cut() {
let err = validate_root_capability_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&role_attestation_capability_proof(PROOF_VERSION_V1),
)
.expect_err("role-attestation capability proofs must fail after the hard cut");
assert_eq!(err.code, ErrorCode::Forbidden);
assert!(
err.message.contains("disabled in 0.65"),
"expected hard-cut rejection, got: {err:?}"
);
}
#[test]
fn validate_root_capability_envelope_rejects_delegated_grant_proof_version_mismatch() {
let request = sample_request(10);
let mut proof = sample_delegated_grant_proof(&request, p(2), p(1), 100 * NS_PER_SEC);
proof.proof_version = PROOF_VERSION_V1 + 1;
let err = validate_root_capability_envelope(
CapabilityService::Root,
CAPABILITY_VERSION_V1,
&delegated_grant_capability_proof(proof),
)
.expect_err("unsupported delegated grant proof version must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn verify_capability_hash_binding_rejects_mismatch() {
let err =
verify_capability_hash_binding(p(1), CAPABILITY_VERSION_V1, &sample_request(10), [0u8; 32])
.expect_err("mismatched hash must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn verify_capability_hash_binding_accepts_match() {
let request = sample_request(10);
let hash = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &request).expect("hash");
verify_capability_hash_binding(p(1), CAPABILITY_VERSION_V1, &request, hash)
.expect("matching hash must verify");
}
#[test]
fn verify_delegated_grant_hash_binding_rejects_mismatch() {
let proof = DelegatedGrantProof {
proof_version: PROOF_VERSION_V1,
capability_hash: [1u8; 32],
grant: crate::dto::capability::DelegatedGrant {
issuer: p(1),
subject: p(2),
audience: vec![p(3)],
scope: crate::dto::capability::DelegatedGrantScope {
service: CapabilityService::Root,
capability_family: "root".to_string(),
},
capability_hash: [2u8; 32],
quota: 1,
issued_at_ns: NS_PER_SEC,
expires_at_ns: 2 * NS_PER_SEC,
epoch: 0,
},
grant_sig: vec![],
key_id: 1,
};
let err = verify_delegated_grant_hash_binding(&proof)
.expect_err("mismatched delegated grant hash must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn delegated_grant_hash_changes_with_payload() {
let grant_a = DelegatedGrant {
issuer: p(1),
subject: p(2),
audience: vec![p(1)],
scope: DelegatedGrantScope {
service: CapabilityService::Root,
capability_family: "RequestCycles".to_string(),
},
capability_hash: [1u8; 32],
quota: 1,
issued_at_ns: 10 * NS_PER_SEC,
expires_at_ns: 20 * NS_PER_SEC,
epoch: 0,
};
let mut grant_b = grant_a.clone();
grant_b.quota = 2;
let hash_a = delegated_grant_hash(&grant_a).expect("hash a");
let hash_b = delegated_grant_hash(&grant_b).expect("hash b");
assert_ne!(hash_a, hash_b);
}
#[test]
fn verify_root_delegated_grant_claims_accepts_matching_scope() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect("matching delegated grant claims must verify");
}
#[test]
fn verify_root_delegated_grant_claims_rejects_subject_mismatch() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.subject = p(3);
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("subject mismatch must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_issuer_mismatch() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.issuer = p(9);
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("issuer mismatch must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_audience_mismatch() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.audience = vec![p(99)];
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("audience mismatch must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_scope_family_mismatch() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.scope.capability_family = "Upgrade".to_string();
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("scope family mismatch must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_zero_quota() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.quota = 0;
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("zero quota must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_not_yet_valid_window() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.issued_at_ns = now_ns + 10 * NS_PER_SEC;
proof.grant.expires_at_ns = now_ns + 20 * NS_PER_SEC;
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("not-yet-valid grant must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_expired_window() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.issued_at_ns = now_ns - 20 * NS_PER_SEC;
proof.grant.expires_at_ns = now_ns - 10 * NS_PER_SEC;
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("expired grant must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_expiry_boundary() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.grant.issued_at_ns = now_ns - 20 * NS_PER_SEC;
proof.grant.expires_at_ns = now_ns;
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("grant at expiry boundary must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
fn verify_root_delegated_grant_claims_rejects_key_id_mismatch() {
let now_ns = 100 * NS_PER_SEC;
let caller = p(2);
let target_canister = p(1);
let capability = sample_request(10);
let mut proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_ns);
proof.key_id = DELEGATED_GRANT_KEY_ID_V1 + 1;
let err =
verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_ns)
.expect_err("unsupported key_id must fail");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
#[test]
#[cfg(feature = "auth-shard-secp256k1-verify")]
fn verify_root_delegated_grant_signature_accepts_valid_signature() {
let capability = sample_request(10);
let proof = sample_delegated_grant_proof(&capability, p(2), p(1), 100 * NS_PER_SEC);
let (public_key, signature) = sign_delegated_grant(7, &proof.grant);
SubnetStateOps::set_delegated_root_public_key("key_1".to_string(), public_key);
verify_root_delegated_grant_signature(&proof.grant, &signature)
.expect("valid delegated grant signature must verify");
}
#[test]
#[cfg(feature = "auth-shard-secp256k1-verify")]
fn verify_root_delegated_grant_signature_rejects_invalid_signature() {
let capability = sample_request(10);
let proof = sample_delegated_grant_proof(&capability, p(2), p(1), 100 * NS_PER_SEC);
let (public_key, _signature) = sign_delegated_grant(7, &proof.grant);
let (_, wrong_signature) = sign_delegated_grant(8, &proof.grant);
SubnetStateOps::set_delegated_root_public_key("key_1".to_string(), public_key);
let err = verify_root_delegated_grant_signature(&proof.grant, &wrong_signature)
.expect_err("invalid signature must fail");
assert_eq!(err.code, ErrorCode::Forbidden);
}
#[test]
#[cfg(not(feature = "auth-shard-secp256k1-verify"))]
fn verify_root_delegated_grant_signature_reports_unavailable_without_verify_feature() {
let capability = sample_request(10);
let proof = sample_delegated_grant_proof(&capability, p(2), p(1), 100 * NS_PER_SEC);
SubnetStateOps::set_delegated_root_public_key("key_1".to_string(), vec![2; 33]);
let err = verify_root_delegated_grant_signature(&proof.grant, &[1])
.expect_err("missing secp256k1 verifier feature must fail closed");
assert_eq!(err.code, ErrorCode::Forbidden);
assert!(
err.message.contains("not enabled") || err.message.contains("unavailable"),
"expected unavailable verifier error, got: {err:?}"
);
}