use std::fmt;
use std::time::Duration;
use crate::crypto::{HMAC_SHA256_TAG_BYTES, HmacSha256Key, HmacSha256Tag};
pub const SECRET_FINGERPRINT_KEY_ID_MIN_BYTES: usize = 1;
pub const SECRET_FINGERPRINT_KEY_ID_MAX_BYTES: usize = 128;
pub const SECRET_FINGERPRINT_TAG_BYTES: usize = HMAC_SHA256_TAG_BYTES;
const SECRET_FINGERPRINT_INPUT_LIMIT_BYTES: usize = 64 * 1024;
const SECRET_FINGERPRINT_DOMAIN: &[u8] = b"auth-00-secret-fingerprint-v1";
pub const DEFAULT_MAXIMUM_STALENESS: Duration = Duration::from_secs(30);
pub const HARD_MAXIMUM_STALENESS: Duration = Duration::from_mins(5);
pub const MAX_VERIFIED_IDENTITY_FIELD_BYTES: usize = 8 * 1024;
pub const MAX_VERIFIED_IDENTITY_CLAIMS: usize = 256;
pub const MAX_VERIFIED_IDENTITY_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IngressFactsError {
KeyIdEmpty,
KeyIdTooLong,
KeyIdNotPrintableAscii,
FingerprintMaterialTooLong,
FingerprintMismatch,
MaximumStalenessAboveCeiling,
MaximumStalenessZero,
IdentityFieldTooLong,
TooManyVerifiedClaims,
IdentityTooLarge,
InvalidVerifiedClaim,
InvalidAudienceBinding,
}
impl fmt::Display for IngressFactsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::KeyIdEmpty => formatter.write_str("secret key identifier must be nonempty"),
Self::KeyIdTooLong => write!(
formatter,
"secret key identifier exceeds {SECRET_FINGERPRINT_KEY_ID_MAX_BYTES} ASCII bytes"
),
Self::KeyIdNotPrintableAscii => formatter.write_str(
"secret key identifier must be printable ASCII; a non-ASCII identifier cannot be \
compared or logged unambiguously",
),
Self::FingerprintMaterialTooLong => write!(
formatter,
"secret fingerprint material exceeds {SECRET_FINGERPRINT_INPUT_LIMIT_BYTES} bytes"
),
Self::FingerprintMismatch => {
formatter.write_str("material does not reproduce the secret fingerprint")
}
Self::MaximumStalenessAboveCeiling => write!(
formatter,
"configured maximum staleness exceeds the {}s hard ceiling",
HARD_MAXIMUM_STALENESS.as_secs()
),
Self::MaximumStalenessZero => {
formatter.write_str("configured maximum staleness must be nonzero")
}
Self::IdentityFieldTooLong => {
formatter.write_str("verified identity field exceeds its byte bound")
}
Self::TooManyVerifiedClaims => {
formatter.write_str("verified identity exceeds its claim-count bound")
}
Self::IdentityTooLarge => {
formatter.write_str("verified identity exceeds its aggregate byte bound")
}
Self::InvalidVerifiedClaim => {
formatter.write_str("verified claim name must be nonempty")
}
Self::InvalidAudienceBinding => {
formatter.write_str("verified audience binding is inconsistent or incomplete")
}
}
}
}
impl std::error::Error for IngressFactsError {}
pub struct SecretFingerprint {
key_id: String,
generation: u64,
tag: HmacSha256Tag,
}
impl Clone for SecretFingerprint {
fn clone(&self) -> Self {
Self {
key_id: self.key_id.clone(),
generation: self.generation,
tag: HmacSha256Tag::from_bytes(*self.tag.as_bytes()),
}
}
}
impl SecretFingerprint {
pub fn derive(
key_id: &str,
generation: u64,
key: &HmacSha256Key,
material: &[u8],
) -> Result<Self, IngressFactsError> {
Self::check_key_id(key_id)?;
if material.len() > SECRET_FINGERPRINT_INPUT_LIMIT_BYTES {
return Err(IngressFactsError::FingerprintMaterialTooLong);
}
let preimage = Self::preimage(key_id, generation, material);
let tag = key
.authenticate_bounded(&preimage, SECRET_FINGERPRINT_INPUT_LIMIT_BYTES * 2)
.map_err(|_| IngressFactsError::FingerprintMaterialTooLong)?;
Ok(Self {
key_id: key_id.to_owned(),
generation,
tag,
})
}
fn check_key_id(key_id: &str) -> Result<(), IngressFactsError> {
if key_id.len() < SECRET_FINGERPRINT_KEY_ID_MIN_BYTES {
return Err(IngressFactsError::KeyIdEmpty);
}
if key_id.len() > SECRET_FINGERPRINT_KEY_ID_MAX_BYTES {
return Err(IngressFactsError::KeyIdTooLong);
}
if !key_id
.bytes()
.all(|byte| byte.is_ascii_graphic() || byte == b' ')
{
return Err(IngressFactsError::KeyIdNotPrintableAscii);
}
Ok(())
}
#[must_use]
pub fn key_id(&self) -> &str {
&self.key_id
}
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub const fn tag(&self) -> &[u8; SECRET_FINGERPRINT_TAG_BYTES] {
self.tag.as_bytes()
}
pub fn verify_material(
&self,
key: &HmacSha256Key,
material: &[u8],
) -> Result<(), IngressFactsError> {
if material.len() > SECRET_FINGERPRINT_INPUT_LIMIT_BYTES {
return Err(IngressFactsError::FingerprintMaterialTooLong);
}
let preimage = Self::preimage(&self.key_id, self.generation, material);
key.verify_bounded(
&preimage,
SECRET_FINGERPRINT_INPUT_LIMIT_BYTES * 2,
&self.tag,
)
.map_err(|_| IngressFactsError::FingerprintMismatch)
}
fn preimage(key_id: &str, generation: u64, material: &[u8]) -> Vec<u8> {
let mut preimage = Vec::with_capacity(material.len() + key_id.len() + 48);
for part in [
SECRET_FINGERPRINT_DOMAIN,
key_id.as_bytes(),
&generation.to_be_bytes(),
material,
] {
preimage.extend_from_slice(&(part.len() as u64).to_be_bytes());
preimage.extend_from_slice(part);
}
preimage
}
}
impl fmt::Debug for SecretFingerprint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretFingerprint")
.field("key_id", &self.key_id)
.field("generation", &self.generation)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReplayPurpose {
EnterpriseIdentityAssertionReplay,
EnterpriseIdJagJtiReplay,
}
impl ReplayPurpose {
pub const ALL: [Self; 2] = [
Self::EnterpriseIdentityAssertionReplay,
Self::EnterpriseIdJagJtiReplay,
];
#[must_use]
pub fn domain(self) -> &'static [u8] {
match self {
Self::EnterpriseIdentityAssertionReplay => {
b"auth-00-replay-enterprise-identity-assertion-v1"
}
Self::EnterpriseIdJagJtiReplay => b"auth-00-replay-enterprise-id-jag-jti-v1",
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::EnterpriseIdentityAssertionReplay => "EnterpriseIdentityAssertionReplay",
Self::EnterpriseIdJagJtiReplay => "EnterpriseIdJagJtiReplay",
}
}
}
impl fmt::Display for ReplayPurpose {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevalidationDispatch {
NotDispatched,
Dispatched,
Unknown,
}
impl fmt::Display for RevalidationDispatch {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::NotDispatched => "NotDispatched",
Self::Dispatched => "Dispatched",
Self::Unknown => "Unknown",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct MaximumStaleness {
bound: Duration,
}
impl MaximumStaleness {
#[must_use]
pub const fn default_bound() -> Self {
Self {
bound: DEFAULT_MAXIMUM_STALENESS,
}
}
pub const fn new(bound: Duration) -> Result<Self, IngressFactsError> {
if bound.is_zero() {
return Err(IngressFactsError::MaximumStalenessZero);
}
if bound.as_nanos() > HARD_MAXIMUM_STALENESS.as_nanos() {
return Err(IngressFactsError::MaximumStalenessAboveCeiling);
}
Ok(Self { bound })
}
#[must_use]
pub const fn bound(self) -> Duration {
self.bound
}
}
impl Default for MaximumStaleness {
fn default() -> Self {
Self::default_bound()
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum VerifiedAudienceBinding {
OAuth {
canonical_resource: String,
validated_audience: String,
audience_policy_id: String,
audience_policy_revision: u64,
provider: String,
configuration_generation: u64,
},
MutualTlsPeer,
StaticCredential,
}
impl VerifiedAudienceBinding {
#[must_use]
pub const fn is_oauth(&self) -> bool {
matches!(self, Self::OAuth { .. })
}
#[must_use]
pub fn audience_policy_id(&self) -> Option<&str> {
match self {
Self::OAuth {
audience_policy_id, ..
} => Some(audience_policy_id),
Self::MutualTlsPeer | Self::StaticCredential => None,
}
}
#[must_use]
pub const fn audience_policy_revision(&self) -> Option<u64> {
match self {
Self::OAuth {
audience_policy_revision,
..
} => Some(*audience_policy_revision),
Self::MutualTlsPeer | Self::StaticCredential => None,
}
}
#[must_use]
pub fn canonical_parts(&self) -> Vec<Vec<u8>> {
match self {
Self::OAuth {
canonical_resource,
validated_audience,
audience_policy_id,
audience_policy_revision,
provider,
configuration_generation,
} => vec![
b"oauth".to_vec(),
canonical_resource.as_bytes().to_vec(),
validated_audience.as_bytes().to_vec(),
audience_policy_id.as_bytes().to_vec(),
audience_policy_revision.to_be_bytes().to_vec(),
provider.as_bytes().to_vec(),
configuration_generation.to_be_bytes().to_vec(),
],
Self::MutualTlsPeer => vec![b"mutual-tls-peer".to_vec()],
Self::StaticCredential => vec![b"static-credential".to_vec()],
}
}
}
impl fmt::Debug for VerifiedAudienceBinding {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OAuth {
audience_policy_id,
audience_policy_revision,
..
} => formatter
.debug_struct("VerifiedAudienceBinding::OAuth")
.field("audience_policy_id", audience_policy_id)
.field("audience_policy_revision", audience_policy_revision)
.finish_non_exhaustive(),
Self::MutualTlsPeer => formatter.write_str("VerifiedAudienceBinding::MutualTlsPeer"),
Self::StaticCredential => {
formatter.write_str("VerifiedAudienceBinding::StaticCredential")
}
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct VerifiedIngressAuthentication {
provider: String,
configuration_generation: u64,
issuer: String,
canonical_resource: String,
verified_audience_binding: VerifiedAudienceBinding,
tenant: String,
subject_or_principal: String,
authorized_party_or_client: String,
verified_claims: Vec<(String, String)>,
auth_policy_revision: u64,
trust_generation: u64,
}
impl VerifiedIngressAuthentication {
pub fn from_verified_provider_output(
facts: VerifiedIdentityFacts<'_>,
) -> Result<Self, IngressFactsError> {
if facts.verified_claims.len() > MAX_VERIFIED_IDENTITY_CLAIMS {
return Err(IngressFactsError::TooManyVerifiedClaims);
}
let mut identity_bytes = 0;
for field in [
facts.provider,
facts.issuer,
facts.canonical_resource,
facts.tenant,
facts.subject_or_principal,
facts.authorized_party_or_client,
] {
if field.is_empty() {
return Err(IngressFactsError::KeyIdEmpty);
}
charge_identity_field(field, &mut identity_bytes)?;
}
if let VerifiedAudienceBinding::OAuth {
canonical_resource,
validated_audience,
audience_policy_id,
provider,
configuration_generation,
..
} = &facts.verified_audience_binding
{
for field in [
canonical_resource,
validated_audience,
audience_policy_id,
provider,
] {
if field.is_empty() {
return Err(IngressFactsError::InvalidAudienceBinding);
}
charge_identity_field(field, &mut identity_bytes)?;
}
if canonical_resource.as_str() != facts.canonical_resource
|| provider.as_str() != facts.provider
|| *configuration_generation != facts.configuration_generation
{
return Err(IngressFactsError::InvalidAudienceBinding);
}
}
for (name, value) in facts.verified_claims {
if name.is_empty() {
return Err(IngressFactsError::InvalidVerifiedClaim);
}
charge_identity_field(name, &mut identity_bytes)?;
charge_identity_field(value, &mut identity_bytes)?;
}
let mut verified_claims: Vec<(String, String)> = facts
.verified_claims
.iter()
.map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
.collect();
verified_claims.sort();
verified_claims.dedup();
Ok(Self {
provider: facts.provider.to_owned(),
configuration_generation: facts.configuration_generation,
issuer: facts.issuer.to_owned(),
canonical_resource: facts.canonical_resource.to_owned(),
verified_audience_binding: facts.verified_audience_binding,
tenant: facts.tenant.to_owned(),
subject_or_principal: facts.subject_or_principal.to_owned(),
authorized_party_or_client: facts.authorized_party_or_client.to_owned(),
verified_claims,
auth_policy_revision: facts.auth_policy_revision,
trust_generation: facts.trust_generation,
})
}
#[must_use]
pub fn provider(&self) -> &str {
&self.provider
}
#[must_use]
pub const fn configuration_generation(&self) -> u64 {
self.configuration_generation
}
#[must_use]
pub fn issuer(&self) -> &str {
&self.issuer
}
#[must_use]
pub fn canonical_resource(&self) -> &str {
&self.canonical_resource
}
#[must_use]
pub const fn verified_audience_binding(&self) -> &VerifiedAudienceBinding {
&self.verified_audience_binding
}
#[must_use]
pub fn tenant(&self) -> &str {
&self.tenant
}
#[must_use]
pub fn subject_or_principal(&self) -> &str {
&self.subject_or_principal
}
#[must_use]
pub fn authorized_party_or_client(&self) -> &str {
&self.authorized_party_or_client
}
#[must_use]
pub fn verified_claims(&self) -> &[(String, String)] {
&self.verified_claims
}
#[must_use]
pub const fn auth_policy_revision(&self) -> u64 {
self.auth_policy_revision
}
#[must_use]
pub const fn trust_generation(&self) -> u64 {
self.trust_generation
}
}
fn charge_identity_field(field: &str, total: &mut usize) -> Result<(), IngressFactsError> {
if field.len() > MAX_VERIFIED_IDENTITY_FIELD_BYTES {
return Err(IngressFactsError::IdentityFieldTooLong);
}
*total = total
.checked_add(field.len())
.ok_or(IngressFactsError::IdentityTooLarge)?;
if *total > MAX_VERIFIED_IDENTITY_BYTES {
return Err(IngressFactsError::IdentityTooLarge);
}
Ok(())
}
impl fmt::Debug for VerifiedIngressAuthentication {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VerifiedIngressAuthentication")
.field("provider", &self.provider)
.field("configuration_generation", &self.configuration_generation)
.field("auth_policy_revision", &self.auth_policy_revision)
.field("trust_generation", &self.trust_generation)
.field("verified_claim_count", &self.verified_claims.len())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct VerifiedIdentityFacts<'a> {
pub provider: &'a str,
pub configuration_generation: u64,
pub issuer: &'a str,
pub canonical_resource: &'a str,
pub verified_audience_binding: VerifiedAudienceBinding,
pub tenant: &'a str,
pub subject_or_principal: &'a str,
pub authorized_party_or_client: &'a str,
pub verified_claims: &'a [(&'a str, &'a str)],
pub auth_policy_revision: u64,
pub trust_generation: u64,
}
impl fmt::Debug for VerifiedIdentityFacts<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VerifiedIdentityFacts")
.field("configuration_generation", &self.configuration_generation)
.field("auth_policy_revision", &self.auth_policy_revision)
.field("trust_generation", &self.trust_generation)
.field("verified_claim_count", &self.verified_claims.len())
.finish_non_exhaustive()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SecurityPartitionDescriptor {
facts: VerifiedIngressAuthentication,
identity: [u8; 32],
}
impl SecurityPartitionDescriptor {
#[must_use]
pub fn from_verified_ingress(facts: &VerifiedIngressAuthentication) -> Self {
let mut parts: Vec<Vec<u8>> = vec![
b"auth-00-security-partition-descriptor-v1".to_vec(),
facts.provider.as_bytes().to_vec(),
facts.configuration_generation.to_be_bytes().to_vec(),
facts.issuer.as_bytes().to_vec(),
facts.canonical_resource.as_bytes().to_vec(),
facts.tenant.as_bytes().to_vec(),
facts.subject_or_principal.as_bytes().to_vec(),
facts.authorized_party_or_client.as_bytes().to_vec(),
facts.auth_policy_revision.to_be_bytes().to_vec(),
facts.trust_generation.to_be_bytes().to_vec(),
];
parts.extend(facts.verified_audience_binding.canonical_parts());
parts.push((facts.verified_claims.len() as u64).to_be_bytes().to_vec());
for (name, value) in &facts.verified_claims {
parts.push(name.as_bytes().to_vec());
parts.push(value.as_bytes().to_vec());
}
let borrowed: Vec<&[u8]> = parts.iter().map(Vec::as_slice).collect();
Self {
facts: facts.clone(),
identity: crate::limits::opaque_admission_digest(&borrowed),
}
}
#[must_use]
pub const fn verified_ingress(&self) -> &VerifiedIngressAuthentication {
&self.facts
}
#[must_use]
pub const fn identity(&self) -> &[u8; 32] {
&self.identity
}
pub fn to_partition_descriptor(
&self,
) -> Result<crate::partition::PartitionDescriptor, crate::limits::SealedAdmissionKeyError> {
let binding = self.facts.verified_audience_binding.canonical_parts();
let binding_parts: Vec<&[u8]> = binding.iter().map(Vec::as_slice).collect();
crate::partition::PartitionDescriptor::from_verified_facts(
&self.facts.provider,
self.facts.configuration_generation,
&self.facts.issuer,
&self.facts.canonical_resource,
&self.facts.tenant,
&self.facts.subject_or_principal,
&self.facts.authorized_party_or_client,
self.facts.trust_generation,
self.facts.auth_policy_revision,
&binding_parts,
)
}
}
impl fmt::Debug for SecurityPartitionDescriptor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecurityPartitionDescriptor")
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct SealedProviderReference {
fingerprint: SecretFingerprint,
}
impl SealedProviderReference {
pub fn seal(
key_id: &str,
generation: u64,
key: &HmacSha256Key,
reference: &[u8],
) -> Result<Self, IngressFactsError> {
Ok(Self {
fingerprint: SecretFingerprint::derive(key_id, generation, key, reference)?,
})
}
#[must_use]
pub const fn fingerprint(&self) -> &SecretFingerprint {
&self.fingerprint
}
pub fn verify_reference(
&self,
key: &HmacSha256Key,
reference: &[u8],
) -> Result<(), IngressFactsError> {
self.fingerprint.verify_material(key, reference)
}
}
impl fmt::Debug for SealedProviderReference {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SealedProviderReference")
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct AuthorizationRotationFacts {
provider_reference: SealedProviderReference,
token_instance: SealedProviderReference,
required_grants: Vec<String>,
trust_generation: u64,
expiry: Duration,
maximum_staleness: MaximumStaleness,
dispatch: RevalidationDispatch,
}
impl AuthorizationRotationFacts {
#[must_use]
pub fn new(
provider_reference: SealedProviderReference,
token_instance: SealedProviderReference,
required_grants: &[&str],
trust_generation: u64,
expiry: Duration,
maximum_staleness: MaximumStaleness,
dispatch: RevalidationDispatch,
) -> Self {
let mut grants: Vec<String> = required_grants
.iter()
.map(|grant| (*grant).to_owned())
.collect();
grants.sort();
grants.dedup();
Self {
provider_reference,
token_instance,
required_grants: grants,
trust_generation,
expiry,
maximum_staleness,
dispatch,
}
}
#[must_use]
pub const fn provider_reference(&self) -> &SealedProviderReference {
&self.provider_reference
}
#[must_use]
pub const fn token_instance(&self) -> &SealedProviderReference {
&self.token_instance
}
#[must_use]
pub fn required_grants(&self) -> &[String] {
&self.required_grants
}
#[must_use]
pub const fn trust_generation(&self) -> u64 {
self.trust_generation
}
#[must_use]
pub const fn expiry(&self) -> Duration {
self.expiry
}
#[must_use]
pub const fn maximum_staleness(&self) -> MaximumStaleness {
self.maximum_staleness
}
#[must_use]
pub const fn dispatch(&self) -> RevalidationDispatch {
self.dispatch
}
#[must_use]
pub fn is_fresh_after(&self, elapsed: Duration) -> bool {
match self.dispatch {
RevalidationDispatch::Dispatched => {
elapsed < self.expiry && elapsed <= self.maximum_staleness.bound()
}
RevalidationDispatch::NotDispatched | RevalidationDispatch::Unknown => false,
}
}
}
impl fmt::Debug for AuthorizationRotationFacts {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AuthorizationRotationFacts")
.field("trust_generation", &self.trust_generation)
.field("required_grant_count", &self.required_grants.len())
.field("maximum_staleness", &self.maximum_staleness)
.field("dispatch", &self.dispatch)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::HMAC_SHA256_KEY_BYTES;
fn identity<'a>(claims: &'a [(&'a str, &'a str)]) -> VerifiedIdentityFacts<'a> {
VerifiedIdentityFacts {
provider: "provider",
configuration_generation: 7,
issuer: "https://issuer.example",
canonical_resource: "https://resource.example/mcp",
verified_audience_binding: VerifiedAudienceBinding::OAuth {
canonical_resource: "https://resource.example/mcp".to_owned(),
validated_audience: "urn:approved-resource-alias".to_owned(),
audience_policy_id: "accepted-audiences".to_owned(),
audience_policy_revision: 3,
provider: "provider".to_owned(),
configuration_generation: 7,
},
tenant: "tenant",
subject_or_principal: "subject",
authorized_party_or_client: "client",
verified_claims: claims,
auth_policy_revision: 11,
trust_generation: 13,
}
}
#[test]
fn admitted_claim_normalization_preserves_partition_identity() {
let first = VerifiedIngressAuthentication::from_verified_provider_output(identity(&[
("scope", "write"),
("scope", "read"),
("scope", "write"),
]))
.unwrap();
let second = VerifiedIngressAuthentication::from_verified_provider_output(identity(&[
("scope", "read"),
("scope", "write"),
]))
.unwrap();
assert_eq!(first, second);
assert_eq!(first.verified_claims().len(), 2);
assert_eq!(
SecurityPartitionDescriptor::from_verified_ingress(&first).identity(),
SecurityPartitionDescriptor::from_verified_ingress(&second).identity()
);
}
#[test]
fn identity_fields_admit_exact_byte_bounds_and_reject_one_byte_more() {
let boundary = "x".repeat(MAX_VERIFIED_IDENTITY_FIELD_BYTES);
let oversized = format!("{boundary}x");
for index in 0..6 {
for (value, accepted) in [(boundary.as_str(), true), (oversized.as_str(), false)] {
let mut facts = identity(&[]);
facts.verified_audience_binding = VerifiedAudienceBinding::StaticCredential;
match index {
0 => facts.provider = value,
1 => facts.issuer = value,
2 => facts.canonical_resource = value,
3 => facts.tenant = value,
4 => facts.subject_or_principal = value,
_ => facts.authorized_party_or_client = value,
}
let result = VerifiedIngressAuthentication::from_verified_provider_output(facts);
if accepted {
assert!(result.is_ok(), "field {index}");
} else {
assert_eq!(result, Err(IngressFactsError::IdentityFieldTooLong));
}
}
}
let unicode = "é".repeat(MAX_VERIFIED_IDENTITY_FIELD_BYTES / 2 + 1);
let mut facts = identity(&[]);
facts.subject_or_principal = &unicode;
assert_eq!(
VerifiedIngressAuthentication::from_verified_provider_output(facts),
Err(IngressFactsError::IdentityFieldTooLong)
);
}
#[test]
fn claim_limits_apply_before_deduplication_and_include_names_and_values() {
let claims = vec![("scope", "read"); MAX_VERIFIED_IDENTITY_CLAIMS];
let admitted =
VerifiedIngressAuthentication::from_verified_provider_output(identity(&claims))
.unwrap();
assert_eq!(admitted.verified_claims().len(), 1);
let mut too_many = claims;
too_many.push(("scope", "read"));
assert_eq!(
VerifiedIngressAuthentication::from_verified_provider_output(identity(&too_many)),
Err(IngressFactsError::TooManyVerifiedClaims)
);
let boundary = "x".repeat(MAX_VERIFIED_IDENTITY_FIELD_BYTES);
let oversized = format!("{boundary}x");
for pair in [(boundary.as_str(), ""), ("scope", boundary.as_str())] {
assert!(
VerifiedIngressAuthentication::from_verified_provider_output(identity(&[pair]))
.is_ok()
);
}
for pair in [(oversized.as_str(), ""), ("scope", oversized.as_str())] {
assert_eq!(
VerifiedIngressAuthentication::from_verified_provider_output(identity(&[pair])),
Err(IngressFactsError::IdentityFieldTooLong)
);
}
assert_eq!(
VerifiedIngressAuthentication::from_verified_provider_output(identity(&[(
"", "value"
)])),
Err(IngressFactsError::InvalidVerifiedClaim)
);
}
#[test]
fn aggregate_identity_budget_counts_duplicate_claims_before_allocation() {
let mut values = vec!["v".repeat(MAX_VERIFIED_IDENTITY_FIELD_BYTES); 7];
let last = MAX_VERIFIED_IDENTITY_BYTES - 14 - 7 * MAX_VERIFIED_IDENTITY_FIELD_BYTES;
values.push("v".repeat(last));
for accepted in [true, false] {
let claims: Vec<(&str, &str)> =
values.iter().map(|value| ("k", value.as_str())).collect();
let mut facts = identity(&claims);
facts.provider = "p";
facts.issuer = "i";
facts.canonical_resource = "r";
facts.tenant = "t";
facts.subject_or_principal = "s";
facts.authorized_party_or_client = "c";
facts.verified_audience_binding = VerifiedAudienceBinding::StaticCredential;
let result = VerifiedIngressAuthentication::from_verified_provider_output(facts);
if accepted {
assert!(result.is_ok());
} else {
assert_eq!(result, Err(IngressFactsError::IdentityTooLarge));
}
values[7].push('v');
}
}
#[test]
fn oauth_binding_rejects_each_contradiction_without_rejecting_approved_aliases() {
assert!(
VerifiedIngressAuthentication::from_verified_provider_output(identity(&[])).is_ok()
);
for mutation in 0..5 {
let mut facts = identity(&[]);
let VerifiedAudienceBinding::OAuth {
canonical_resource,
validated_audience,
audience_policy_id,
provider,
configuration_generation,
..
} = &mut facts.verified_audience_binding
else {
panic!("OAuth fixture");
};
match mutation {
0 => canonical_resource.push_str("/other"),
1 => provider.push_str("-other"),
2 => *configuration_generation += 1,
3 => validated_audience.clear(),
_ => audience_policy_id.clear(),
}
assert_eq!(
VerifiedIngressAuthentication::from_verified_provider_output(facts),
Err(IngressFactsError::InvalidAudienceBinding)
);
}
for binding in [
VerifiedAudienceBinding::MutualTlsPeer,
VerifiedAudienceBinding::StaticCredential,
] {
let mut facts = identity(&[]);
facts.verified_audience_binding = binding;
assert!(VerifiedIngressAuthentication::from_verified_provider_output(facts).is_ok());
}
}
#[test]
fn oauth_audience_and_policy_strings_are_bounded() {
for policy in [false, true] {
let mut facts = identity(&[]);
let VerifiedAudienceBinding::OAuth {
validated_audience,
audience_policy_id,
..
} = &mut facts.verified_audience_binding
else {
panic!("OAuth fixture");
};
let field = if policy {
audience_policy_id
} else {
validated_audience
};
*field = "x".repeat(MAX_VERIFIED_IDENTITY_FIELD_BYTES);
assert!(
VerifiedIngressAuthentication::from_verified_provider_output(facts.clone()).is_ok()
);
let VerifiedAudienceBinding::OAuth {
validated_audience,
audience_policy_id,
..
} = &mut facts.verified_audience_binding
else {
panic!("OAuth fixture");
};
let field = if policy {
audience_policy_id
} else {
validated_audience
};
field.push('x');
assert_eq!(
VerifiedIngressAuthentication::from_verified_provider_output(facts),
Err(IngressFactsError::IdentityFieldTooLong)
);
}
}
#[test]
fn borrowed_identity_debug_redacts_even_unadmitted_provider_input() {
let canary = "IDENTITY-SECRET-CANARY";
let claims = [(canary, canary)];
let mut facts = identity(&claims);
facts.provider = canary;
facts.issuer = canary;
facts.canonical_resource = canary;
facts.tenant = canary;
facts.subject_or_principal = canary;
facts.authorized_party_or_client = canary;
facts.verified_audience_binding = VerifiedAudienceBinding::OAuth {
canonical_resource: canary.to_owned(),
validated_audience: canary.to_owned(),
audience_policy_id: canary.to_owned(),
audience_policy_revision: 1,
provider: canary.to_owned(),
configuration_generation: 7,
};
let diagnostic = format!("{facts:?}");
assert!(!diagnostic.contains(canary));
assert!(diagnostic.contains("verified_claim_count: 1"));
}
fn rotation(expiry: Duration, dispatch: RevalidationDispatch) -> AuthorizationRotationFacts {
let key = HmacSha256Key::from_bytes([42; HMAC_SHA256_KEY_BYTES]);
let provider = SealedProviderReference::seal("test-key", 1, &key, b"provider").unwrap();
let token = SealedProviderReference::seal("test-key", 1, &key, b"token").unwrap();
AuthorizationRotationFacts::new(
provider,
token,
&["mcp.read"],
1,
expiry,
MaximumStaleness::default(),
dispatch,
)
}
#[test]
fn authorization_expiry_is_independent_of_revalidation_staleness() {
let expiry = Duration::from_secs(5);
let facts = rotation(expiry, RevalidationDispatch::Dispatched);
assert!(facts.is_fresh_after(expiry - Duration::from_nanos(1)));
assert!(!facts.is_fresh_after(expiry));
assert!(!facts.is_fresh_after(expiry + Duration::from_nanos(1)));
assert!(
!rotation(Duration::ZERO, RevalidationDispatch::Dispatched)
.is_fresh_after(Duration::ZERO)
);
let long_lived = rotation(Duration::from_secs(600), RevalidationDispatch::Dispatched);
assert!(long_lived.is_fresh_after(DEFAULT_MAXIMUM_STALENESS));
assert!(!long_lived.is_fresh_after(DEFAULT_MAXIMUM_STALENESS + Duration::from_nanos(1)));
assert!(!long_lived.is_fresh_after(Duration::MAX));
for dispatch in [
RevalidationDispatch::Unknown,
RevalidationDispatch::NotDispatched,
] {
assert!(!rotation(Duration::from_secs(600), dispatch).is_fresh_after(Duration::ZERO));
}
}
}