riffdb_types/
capability.rs1use std::fmt;
4
5use crate::{DIGEST_SCHEME_V1, DigestKeyId};
6
7pub const MAX_CAPABILITY_AUDIENCES: usize = 8;
9pub const MAX_CAPABILITY_LIFETIME_SECONDS: u32 = 2_592_000;
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub enum RevocationReasonCodeV1 {
15 Requested,
17 Replaced,
19 SuspectedCompromise,
21 PolicyChange,
23}
24
25impl RevocationReasonCodeV1 {
26 #[must_use]
28 pub const fn tag(self) -> u8 {
29 match self {
30 Self::Requested => 0x01,
31 Self::Replaced => 0x02,
32 Self::SuspectedCompromise => 0x03,
33 Self::PolicyChange => 0x04,
34 }
35 }
36
37 #[must_use]
39 pub const fn from_tag(tag: u8) -> Option<Self> {
40 match tag {
41 0x01 => Some(Self::Requested),
42 0x02 => Some(Self::Replaced),
43 0x03 => Some(Self::SuspectedCompromise),
44 0x04 => Some(Self::PolicyChange),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub struct CapabilityTokenDigest {
58 key_id: DigestKeyId,
59 bytes: [u8; 32],
60}
61
62impl CapabilityTokenDigest {
63 #[must_use]
65 pub const fn from_hmac_bytes(key_id: DigestKeyId, bytes: [u8; 32]) -> Self {
66 Self { key_id, bytes }
67 }
68
69 #[must_use]
71 pub const fn scheme(self) -> u8 {
72 DIGEST_SCHEME_V1
73 }
74
75 #[must_use]
77 pub const fn key_id(self) -> DigestKeyId {
78 self.key_id
79 }
80
81 #[must_use]
83 pub const fn as_bytes(&self) -> &[u8; 32] {
84 &self.bytes
85 }
86}
87
88impl fmt::Debug for CapabilityTokenDigest {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter
91 .debug_struct("CapabilityTokenDigest")
92 .field("scheme", &DIGEST_SCHEME_V1)
93 .field("key_id", &self.key_id)
94 .field("bytes", &"[REDACTED]")
95 .finish()
96 }
97}
98
99impl fmt::Display for CapabilityTokenDigest {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str("capability token digest [REDACTED]")
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn digest_key_id(value: u32) -> DigestKeyId {
110 DigestKeyId::try_from(value).expect("nonzero digest key ID")
111 }
112
113 #[test]
114 fn formatting_redacts_digest_bytes() {
115 let digest = CapabilityTokenDigest::from_hmac_bytes(digest_key_id(7), [0xab; 32]);
116 let debug = format!("{digest:?}");
117 let display = digest.to_string();
118
119 assert!(debug.contains("[REDACTED]"));
120 assert!(display.contains("[REDACTED]"));
121 assert!(!debug.contains("abababab"));
122 assert!(!display.contains("abababab"));
123 assert_eq!(digest.scheme(), DIGEST_SCHEME_V1);
124 assert_eq!(digest.key_id(), digest_key_id(7));
125 assert_eq!(digest.as_bytes(), &[0xab; 32]);
126 }
127
128 #[test]
129 fn capability_hard_limits_have_exact_v1_values() {
130 assert_eq!(MAX_CAPABILITY_AUDIENCES, 8);
131 assert_eq!(MAX_CAPABILITY_LIFETIME_SECONDS, 2_592_000);
132 }
133
134 #[test]
135 fn revocation_reason_registry_has_exact_v1_tags() {
136 let registry = [
137 (RevocationReasonCodeV1::Requested, 0x01),
138 (RevocationReasonCodeV1::Replaced, 0x02),
139 (RevocationReasonCodeV1::SuspectedCompromise, 0x03),
140 (RevocationReasonCodeV1::PolicyChange, 0x04),
141 ];
142
143 for (reason, tag) in registry {
144 assert_eq!(reason.tag(), tag);
145 assert_eq!(RevocationReasonCodeV1::from_tag(tag), Some(reason));
146 }
147 assert_eq!(RevocationReasonCodeV1::from_tag(0), None);
148 assert_eq!(RevocationReasonCodeV1::from_tag(5), None);
149 assert_eq!(RevocationReasonCodeV1::from_tag(u8::MAX), None);
150 }
151}