use super::{
AuthOps, DelegatedTokenVerifierConfig, PreparedDelegatedTokenSignature,
SignDelegatedTokenInput, VerifyDelegatedTokenRuntimeInput,
delegated::mint::{
MintDelegatedTokenError, MintDelegatedTokenInput, finish_delegated_token,
prepare_delegated_token,
},
delegated::{
canonical::{derivation_path_hash, key_name_hash},
cert_rules::DelegatedAuthTtlLimits,
verify::{
VerifiedDelegatedToken, VerifyDelegatedTokenError, VerifyDelegatedTokenInput,
verify_delegated_token,
},
},
keys,
root_canister_sig::{IC_ROOT_PUBLIC_KEY_RAW_LENGTH, RootPayloadKind},
};
use crate::{
InternalError,
cdk::{types::Principal, utils::hash::decode_hex},
config::schema::DelegatedTokenConfig,
dto::auth::{DelegatedToken, ShardKeyBinding},
ops::{
auth::{AuthScopeError, AuthValidationError},
config::ConfigOps,
cost_guard::CostGuardPermit,
ic::{IcOps, ecdsa::EcdsaOps},
replay::model::{EcdsaPurpose, ExternalEffectDescriptor},
runtime::{
env::EnvOps,
metrics::delegated_auth::{DelegatedAuthMetricReason, DelegatedAuthMetrics},
},
},
};
impl AuthOps {
pub(crate) fn prepare_delegated_token_signature(
input: SignDelegatedTokenInput,
) -> Result<PreparedDelegatedTokenSignature, InternalError> {
let local = IcOps::canister_self();
if input.proof.cert.shard_pid != local {
return Err(AuthScopeError::ShardPidMismatch {
expected: local,
found: input.proof.cert.shard_pid,
}
.into());
}
let prepared = prepare_delegated_token(MintDelegatedTokenInput {
proof: &input.proof,
subject: input.subject,
audience: input.audience,
grants: input.grants,
ttl_ns: input.ttl_ns,
nonce: input.nonce,
now_ns: IcOps::now_nanos(),
})
.map_err(map_mint_delegated_token_error)?;
let key_name = keys::delegated_tokens_key_name()?;
let derivation_path = keys::shard_derivation_path(local);
Ok(PreparedDelegatedTokenSignature {
message_hash: prepared.shard_token_hash,
prepared,
key_name,
derivation_path,
})
}
pub(crate) async fn sign_prepared_delegated_token(
permit: &CostGuardPermit,
prepared: PreparedDelegatedTokenSignature,
) -> Result<DelegatedToken, InternalError> {
let PreparedDelegatedTokenSignature {
prepared,
message_hash,
key_name,
derivation_path,
} = prepared;
DelegatedAuthMetrics::record_shard_token_sign_started();
let shard_sig =
match EcdsaOps::sign_bytes(permit, &key_name, derivation_path, message_hash).await {
Ok(signature) => signature,
Err(err) => {
DelegatedAuthMetrics::record_shard_token_sign_failed();
return Err(err);
}
};
DelegatedAuthMetrics::record_shard_token_sign_completed();
Ok(finish_delegated_token(prepared, shard_sig))
}
pub(crate) fn delegated_token_signing_effect(
prepared: &PreparedDelegatedTokenSignature,
) -> ExternalEffectDescriptor {
ExternalEffectDescriptor::ThresholdEcdsaSign {
key_id_hash: key_name_hash(&prepared.key_name),
purpose: EcdsaPurpose::DelegatedToken,
message_hash: prepared.message_hash,
}
}
pub(crate) fn delegated_token_verifier_config()
-> Result<DelegatedTokenVerifierConfig, InternalError> {
let cfg = ConfigOps::delegated_tokens_config()?;
Self::delegated_token_verifier_config_from(&cfg)
}
pub fn verify_token(
input: VerifyDelegatedTokenRuntimeInput<'_>,
) -> Result<VerifiedDelegatedToken, InternalError> {
DelegatedAuthMetrics::record_verify_started();
let cfg = match ConfigOps::delegated_tokens_config() {
Ok(cfg) => cfg,
Err(err) => {
DelegatedAuthMetrics::record_verify_failed(DelegatedAuthMetricReason::InvalidState);
return Err(err);
}
};
if !cfg.enabled {
DelegatedAuthMetrics::record_verify_failed(DelegatedAuthMetricReason::Disabled);
return Err(AuthValidationError::DelegatedTokenAuthDisabled.into());
}
let verifier_cfg = match Self::delegated_token_verifier_config_from(&cfg) {
Ok(verifier_cfg) => verifier_cfg,
Err(err) => {
DelegatedAuthMetrics::record_verify_failed(DelegatedAuthMetricReason::RootKey);
return Err(err);
}
};
if let Err(err) = Self::verify_shard_key_binding(input.token) {
DelegatedAuthMetrics::record_verify_failed(DelegatedAuthMetricReason::ShardKeyBinding);
return Err(err);
}
let local_role = match EnvOps::canister_role() {
Ok(local_role) => local_role,
Err(err) => {
DelegatedAuthMetrics::record_verify_failed(DelegatedAuthMetricReason::InvalidState);
return Err(err);
}
};
let local_project = match ConfigOps::get() {
Ok(cfg) => cfg.fleet_name().map(ToOwned::to_owned),
Err(err) => {
DelegatedAuthMetrics::record_verify_failed(DelegatedAuthMetricReason::InvalidState);
return Err(err);
}
};
let verified = verify_delegated_token(
VerifyDelegatedTokenInput {
token: input.token,
local_role: Some(&local_role),
local_project: local_project.as_deref(),
ttl_limits: DelegatedAuthTtlLimits {
max_cert_ttl_ns: input.max_cert_ttl_ns,
max_token_ttl_ns: input.max_token_ttl_ns,
},
required_scopes: input.required_scopes,
now_ns: input.now_ns,
},
|cert_hash, root_proof, root_pid| {
if root_pid != verifier_cfg.root_canister_id {
return Err(AuthValidationError::InvalidRootAuthority {
expected: verifier_cfg.root_canister_id,
found: root_pid,
}
.to_string());
}
Self::verify_root_canister_signature_proof(
RootPayloadKind::DelegationCert,
cert_hash,
root_proof,
verifier_cfg.root_canister_id,
&verifier_cfg.ic_root_public_key_raw,
)
.map_err(|err| err.to_string())
},
|public_key, hash, sig| {
if public_key.len() != 33 {
return Err(
"delegated auth shard public key is not compressed SEC1".to_string()
);
}
EcdsaOps::verify_signature(public_key, hash, sig).map_err(|err| err.to_string())
},
)
.map_err(|err| {
DelegatedAuthMetrics::record_verify_failed(delegated_auth_reason_from_verify_error(
&err,
));
map_verify_delegated_token_error(err)
})?;
DelegatedAuthMetrics::record_verify_completed();
Ok(verified)
}
fn delegated_token_verifier_config_from(
cfg: &DelegatedTokenConfig,
) -> Result<DelegatedTokenVerifierConfig, InternalError> {
Ok(DelegatedTokenVerifierConfig {
root_canister_id: configured_root_canister_id(cfg)?,
ic_root_public_key_raw: configured_ic_root_public_key_raw(cfg)?,
})
}
fn verify_shard_key_binding(token: &DelegatedToken) -> Result<(), InternalError> {
let cert = &token.proof.cert;
let key_name = keys::delegated_tokens_key_name()?;
let expected_derivation_path_hash =
derivation_path_hash(&keys::shard_derivation_path(cert.shard_pid));
match cert.shard_key_binding {
ShardKeyBinding::IcThresholdEcdsaSecp256k1 {
key_name_hash: observed_key_name_hash,
derivation_path_hash: observed_derivation_path_hash,
} => {
if observed_key_name_hash != key_name_hash(&key_name)
|| observed_derivation_path_hash != expected_derivation_path_hash
{
return Err(AuthValidationError::Auth(
"delegated auth shard key binding mismatch".to_string(),
)
.into());
}
}
}
Ok(())
}
}
fn configured_root_canister_id(cfg: &DelegatedTokenConfig) -> Result<Principal, InternalError> {
let Some(root_canister_id) = cfg.root_canister_id.as_deref() else {
return EnvOps::root_pid();
};
let root_canister_id = root_canister_id.trim();
if root_canister_id.is_empty() {
return Err(AuthValidationError::Auth(
"auth.delegated_tokens.root_canister_id must not be empty when set".to_string(),
)
.into());
}
Principal::from_text(root_canister_id).map_err(|err| {
AuthValidationError::Auth(format!(
"auth.delegated_tokens.root_canister_id is not a valid principal: {err}"
))
.into()
})
}
fn configured_ic_root_public_key_raw(cfg: &DelegatedTokenConfig) -> Result<Vec<u8>, InternalError> {
let ic_root_public_key_raw = match cfg.ic_root_public_key_raw_hex.as_deref() {
Some(root_key_hex) => {
let root_key_hex = root_key_hex.trim();
if root_key_hex.is_empty() {
return Err(AuthValidationError::Auth(
"auth.delegated_tokens.ic_root_public_key_raw_hex must not be empty when set"
.to_string(),
)
.into());
}
decode_hex(root_key_hex).map_err(|err| {
AuthValidationError::Auth(format!(
"auth.delegated_tokens.ic_root_public_key_raw_hex is not valid hex: {err}"
))
})?
}
None => AuthOps::ic_root_public_key_raw()?,
};
if ic_root_public_key_raw.len() != IC_ROOT_PUBLIC_KEY_RAW_LENGTH {
return Err(AuthValidationError::Auth(format!(
"auth.delegated_tokens.ic_root_public_key_raw must be {IC_ROOT_PUBLIC_KEY_RAW_LENGTH} raw bytes"
))
.into());
}
Ok(ic_root_public_key_raw)
}
fn map_mint_delegated_token_error(err: MintDelegatedTokenError) -> InternalError {
AuthValidationError::Auth(err.to_string()).into()
}
fn map_verify_delegated_token_error(err: VerifyDelegatedTokenError) -> InternalError {
AuthValidationError::Auth(err.to_string()).into()
}
const fn delegated_auth_reason_from_verify_error(
err: &VerifyDelegatedTokenError,
) -> DelegatedAuthMetricReason {
match err {
VerifyDelegatedTokenError::Audience(_) => DelegatedAuthMetricReason::Audience,
VerifyDelegatedTokenError::AudienceNotSubset => {
DelegatedAuthMetricReason::AudienceNotSubset
}
VerifyDelegatedTokenError::Canonical(_) => DelegatedAuthMetricReason::Canonical,
VerifyDelegatedTokenError::CertAudienceRejected => {
DelegatedAuthMetricReason::CertAudienceRejected
}
VerifyDelegatedTokenError::CertExpired => DelegatedAuthMetricReason::CertExpired,
VerifyDelegatedTokenError::CertHashMismatch => DelegatedAuthMetricReason::CertHashMismatch,
VerifyDelegatedTokenError::CertNotYetValid => DelegatedAuthMetricReason::CertNotYetValid,
VerifyDelegatedTokenError::CertRules(_) => DelegatedAuthMetricReason::CertPolicy,
VerifyDelegatedTokenError::IssuerShardPidMismatch => {
DelegatedAuthMetricReason::IssuerShardPidMismatch
}
VerifyDelegatedTokenError::GrantsNotSubset => DelegatedAuthMetricReason::GrantsNotSubset,
VerifyDelegatedTokenError::MissingLocalRole => DelegatedAuthMetricReason::MissingLocalRole,
VerifyDelegatedTokenError::RootSignatureInvalid(_) => {
DelegatedAuthMetricReason::RootSignatureInvalid
}
VerifyDelegatedTokenError::ScopeRejected { .. } => DelegatedAuthMetricReason::ScopeRejected,
VerifyDelegatedTokenError::ShardSignatureInvalid(_) => {
DelegatedAuthMetricReason::ShardSignatureInvalid
}
VerifyDelegatedTokenError::ShardSignatureUnavailable => {
DelegatedAuthMetricReason::ShardSignatureUnavailable
}
VerifyDelegatedTokenError::TokenAudienceRejected => {
DelegatedAuthMetricReason::TokenAudienceRejected
}
VerifyDelegatedTokenError::TokenExpired => DelegatedAuthMetricReason::TokenExpired,
VerifyDelegatedTokenError::TokenGrantRejected => {
DelegatedAuthMetricReason::TokenGrantRejected
}
VerifyDelegatedTokenError::TokenInvalidWindow => {
DelegatedAuthMetricReason::TokenInvalidWindow
}
VerifyDelegatedTokenError::TokenIssuedBeforeCert => {
DelegatedAuthMetricReason::TokenIssuedBeforeCert
}
VerifyDelegatedTokenError::TokenNotYetValid => DelegatedAuthMetricReason::TokenNotYetValid,
VerifyDelegatedTokenError::TokenOutlivesCert => {
DelegatedAuthMetricReason::TokenOutlivesCert
}
VerifyDelegatedTokenError::TokenTtlExceeded { .. } => {
DelegatedAuthMetricReason::TokenTtlExceeded
}
}
}