use std::fmt;
use crate::{DIGEST_SCHEME_V1, DigestKeyId};
pub const MAX_CAPABILITY_AUDIENCES: usize = 8;
pub const MAX_CAPABILITY_LIFETIME_SECONDS: u32 = 2_592_000;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RevocationReasonCodeV1 {
Requested,
Replaced,
SuspectedCompromise,
PolicyChange,
}
impl RevocationReasonCodeV1 {
#[must_use]
pub const fn tag(self) -> u8 {
match self {
Self::Requested => 0x01,
Self::Replaced => 0x02,
Self::SuspectedCompromise => 0x03,
Self::PolicyChange => 0x04,
}
}
#[must_use]
pub const fn from_tag(tag: u8) -> Option<Self> {
match tag {
0x01 => Some(Self::Requested),
0x02 => Some(Self::Replaced),
0x03 => Some(Self::SuspectedCompromise),
0x04 => Some(Self::PolicyChange),
_ => None,
}
}
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CapabilityTokenDigest {
key_id: DigestKeyId,
bytes: [u8; 32],
}
impl CapabilityTokenDigest {
#[must_use]
pub const fn from_hmac_bytes(key_id: DigestKeyId, bytes: [u8; 32]) -> Self {
Self { key_id, bytes }
}
#[must_use]
pub const fn scheme(self) -> u8 {
DIGEST_SCHEME_V1
}
#[must_use]
pub const fn key_id(self) -> DigestKeyId {
self.key_id
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.bytes
}
}
impl fmt::Debug for CapabilityTokenDigest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CapabilityTokenDigest")
.field("scheme", &DIGEST_SCHEME_V1)
.field("key_id", &self.key_id)
.field("bytes", &"[REDACTED]")
.finish()
}
}
impl fmt::Display for CapabilityTokenDigest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("capability token digest [REDACTED]")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn digest_key_id(value: u32) -> DigestKeyId {
DigestKeyId::try_from(value).expect("nonzero digest key ID")
}
#[test]
fn formatting_redacts_digest_bytes() {
let digest = CapabilityTokenDigest::from_hmac_bytes(digest_key_id(7), [0xab; 32]);
let debug = format!("{digest:?}");
let display = digest.to_string();
assert!(debug.contains("[REDACTED]"));
assert!(display.contains("[REDACTED]"));
assert!(!debug.contains("abababab"));
assert!(!display.contains("abababab"));
assert_eq!(digest.scheme(), DIGEST_SCHEME_V1);
assert_eq!(digest.key_id(), digest_key_id(7));
assert_eq!(digest.as_bytes(), &[0xab; 32]);
}
#[test]
fn capability_hard_limits_have_exact_v1_values() {
assert_eq!(MAX_CAPABILITY_AUDIENCES, 8);
assert_eq!(MAX_CAPABILITY_LIFETIME_SECONDS, 2_592_000);
}
#[test]
fn revocation_reason_registry_has_exact_v1_tags() {
let registry = [
(RevocationReasonCodeV1::Requested, 0x01),
(RevocationReasonCodeV1::Replaced, 0x02),
(RevocationReasonCodeV1::SuspectedCompromise, 0x03),
(RevocationReasonCodeV1::PolicyChange, 0x04),
];
for (reason, tag) in registry {
assert_eq!(reason.tag(), tag);
assert_eq!(RevocationReasonCodeV1::from_tag(tag), Some(reason));
}
assert_eq!(RevocationReasonCodeV1::from_tag(0), None);
assert_eq!(RevocationReasonCodeV1::from_tag(5), None);
assert_eq!(RevocationReasonCodeV1::from_tag(u8::MAX), None);
}
}