1use crate::error::AttestationError;
4use crate::types::{DeviceDID, IdentityDID};
5use chrono::{DateTime, Utc};
6use hex;
7use json_canon;
8use log::debug;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::fmt;
12use std::ops::Deref;
13use std::str::FromStr;
14
15pub const MAX_ATTESTATION_JSON_SIZE: usize = 64 * 1024;
17
18pub const MAX_JSON_BATCH_SIZE: usize = 1024 * 1024;
20
21pub const MAX_PUBLIC_KEY_HEX_LEN: usize = 64;
23pub const MAX_SIGNATURE_HEX_LEN: usize = 128;
25pub const MAX_FILE_HASH_HEX_LEN: usize = 64;
27
28const SIGN_COMMIT: &str = "sign_commit";
30const SIGN_RELEASE: &str = "sign_release";
31const MANAGE_MEMBERS: &str = "manage_members";
32const ROTATE_KEYS: &str = "rotate_keys";
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(transparent)]
46pub struct ResourceId(String);
47
48impl ResourceId {
49 pub fn new(s: impl Into<String>) -> Self {
51 Self(s.into())
52 }
53
54 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58}
59
60impl Deref for ResourceId {
61 type Target = str;
62 fn deref(&self) -> &str {
63 &self.0
64 }
65}
66
67impl fmt::Display for ResourceId {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.write_str(&self.0)
70 }
71}
72
73impl From<String> for ResourceId {
74 fn from(s: String) -> Self {
75 Self(s)
76 }
77}
78
79impl From<&str> for ResourceId {
80 fn from(s: &str) -> Self {
81 Self(s.to_string())
82 }
83}
84
85impl PartialEq<str> for ResourceId {
86 fn eq(&self, other: &str) -> bool {
87 self.0 == other
88 }
89}
90
91impl PartialEq<&str> for ResourceId {
92 fn eq(&self, other: &&str) -> bool {
93 self.0 == *other
94 }
95}
96
97impl PartialEq<String> for ResourceId {
98 fn eq(&self, other: &String) -> bool {
99 self.0 == *other
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[serde(rename_all = "lowercase")]
114pub enum Role {
115 Admin,
117 Member,
119 Readonly,
121}
122
123impl Role {
124 pub fn as_str(&self) -> &str {
126 match self {
127 Role::Admin => "admin",
128 Role::Member => "member",
129 Role::Readonly => "readonly",
130 }
131 }
132
133 pub fn default_capabilities(&self) -> Vec<Capability> {
135 match self {
136 Role::Admin => vec![
137 Capability::sign_commit(),
138 Capability::sign_release(),
139 Capability::manage_members(),
140 Capability::rotate_keys(),
141 ],
142 Role::Member => vec![Capability::sign_commit(), Capability::sign_release()],
143 Role::Readonly => vec![],
144 }
145 }
146}
147
148impl fmt::Display for Role {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(self.as_str())
151 }
152}
153
154impl FromStr for Role {
155 type Err = RoleParseError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 match s.trim().to_lowercase().as_str() {
159 "admin" => Ok(Role::Admin),
160 "member" => Ok(Role::Member),
161 "readonly" => Ok(Role::Readonly),
162 other => Err(RoleParseError(other.to_string())),
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
169#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
170pub struct RoleParseError(String);
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub struct Ed25519PublicKey([u8; 32]);
182
183impl Ed25519PublicKey {
184 pub fn from_bytes(bytes: [u8; 32]) -> Self {
186 Self(bytes)
187 }
188
189 pub fn try_from_slice(slice: &[u8]) -> Result<Self, Ed25519KeyError> {
199 let arr: [u8; 32] = slice
200 .try_into()
201 .map_err(|_| Ed25519KeyError::InvalidLength(slice.len()))?;
202 Ok(Self(arr))
203 }
204
205 pub fn as_bytes(&self) -> &[u8; 32] {
207 &self.0
208 }
209
210 pub fn is_zero(&self) -> bool {
212 self.0 == [0u8; 32]
213 }
214}
215
216impl Serialize for Ed25519PublicKey {
217 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
218 s.serialize_str(&hex::encode(self.0))
219 }
220}
221
222impl<'de> Deserialize<'de> for Ed25519PublicKey {
223 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
224 let s = String::deserialize(d)?;
225 let bytes =
226 hex::decode(&s).map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
227 let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
228 serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len()))
229 })?;
230 Ok(Self(arr))
231 }
232}
233
234impl fmt::Display for Ed25519PublicKey {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 f.write_str(&hex::encode(self.0))
237 }
238}
239
240impl AsRef<[u8]> for Ed25519PublicKey {
241 fn as_ref(&self) -> &[u8] {
242 &self.0
243 }
244}
245
246#[cfg(feature = "schema")]
247impl schemars::JsonSchema for Ed25519PublicKey {
248 fn schema_name() -> String {
249 "Ed25519PublicKey".to_owned()
250 }
251
252 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
253 schemars::schema::SchemaObject {
254 instance_type: Some(schemars::schema::InstanceType::String.into()),
255 format: Some("hex".to_owned()),
256 metadata: Some(Box::new(schemars::schema::Metadata {
257 description: Some("Ed25519 public key (32 bytes, hex-encoded)".to_owned()),
258 ..Default::default()
259 })),
260 ..Default::default()
261 }
262 .into()
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
268pub enum Ed25519KeyError {
269 #[error("expected 32 bytes, got {0}")]
271 InvalidLength(usize),
272 #[error("invalid hex: {0}")]
274 InvalidHex(String),
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct Ed25519Signature([u8; 64]);
284
285impl Ed25519Signature {
286 pub fn from_bytes(bytes: [u8; 64]) -> Self {
288 Self(bytes)
289 }
290
291 pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
293 let arr: [u8; 64] = slice
294 .try_into()
295 .map_err(|_| SignatureLengthError(slice.len()))?;
296 Ok(Self(arr))
297 }
298
299 pub fn empty() -> Self {
301 Self([0u8; 64])
302 }
303
304 pub fn is_empty(&self) -> bool {
306 self.0 == [0u8; 64]
307 }
308
309 pub fn as_bytes(&self) -> &[u8; 64] {
311 &self.0
312 }
313}
314
315impl Default for Ed25519Signature {
316 fn default() -> Self {
317 Self::empty()
318 }
319}
320
321impl std::fmt::Display for Ed25519Signature {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(f, "{}", hex::encode(self.0))
324 }
325}
326
327impl AsRef<[u8]> for Ed25519Signature {
328 fn as_ref(&self) -> &[u8] {
329 &self.0
330 }
331}
332
333#[cfg(feature = "schema")]
334impl schemars::JsonSchema for Ed25519Signature {
335 fn schema_name() -> String {
336 "Ed25519Signature".to_owned()
337 }
338
339 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
340 schemars::schema::SchemaObject {
341 instance_type: Some(schemars::schema::InstanceType::String.into()),
342 format: Some("hex".to_owned()),
343 metadata: Some(Box::new(schemars::schema::Metadata {
344 description: Some("Ed25519 signature (64 bytes, hex-encoded)".to_owned()),
345 ..Default::default()
346 })),
347 ..Default::default()
348 }
349 .into()
350 }
351}
352
353impl serde::Serialize for Ed25519Signature {
354 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
355 serializer.serialize_str(&hex::encode(self.0))
356 }
357}
358
359impl<'de> serde::Deserialize<'de> for Ed25519Signature {
360 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
361 let s = String::deserialize(deserializer)?;
362 if s.is_empty() {
363 return Ok(Self::empty());
364 }
365 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
366 Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
372#[error("expected 64 bytes, got {0}")]
373pub struct SignatureLengthError(pub usize);
374
375#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
381pub enum CapabilityError {
382 #[error("capability is empty")]
384 Empty,
385 #[error("capability exceeds 64 chars: {0}")]
387 TooLong(usize),
388 #[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
390 InvalidChars(String),
391 #[error(
393 "reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
394 )]
395 ReservedNamespace,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
427#[serde(try_from = "String", into = "String")]
428pub struct Capability(String);
429
430impl Capability {
431 pub const MAX_LEN: usize = 64;
433
434 const RESERVED_PREFIX: &'static str = "auths:";
436
437 #[inline]
445 pub fn sign_commit() -> Self {
446 Self(SIGN_COMMIT.to_string())
447 }
448
449 #[inline]
453 pub fn sign_release() -> Self {
454 Self(SIGN_RELEASE.to_string())
455 }
456
457 #[inline]
461 pub fn manage_members() -> Self {
462 Self(MANAGE_MEMBERS.to_string())
463 }
464
465 #[inline]
469 pub fn rotate_keys() -> Self {
470 Self(ROTATE_KEYS.to_string())
471 }
472
473 pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
505 let canonical = raw.trim().to_lowercase();
506
507 if canonical.is_empty() {
508 return Err(CapabilityError::Empty);
509 }
510 if canonical.len() > Self::MAX_LEN {
511 return Err(CapabilityError::TooLong(canonical.len()));
512 }
513 if !canonical
514 .chars()
515 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
516 {
517 return Err(CapabilityError::InvalidChars(canonical));
518 }
519 if canonical.starts_with(Self::RESERVED_PREFIX) {
520 return Err(CapabilityError::ReservedNamespace);
521 }
522
523 Ok(Self(canonical))
524 }
525
526 #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
534 pub fn custom(s: impl Into<String>) -> Option<Self> {
535 Self::parse(&s.into()).ok()
536 }
537
538 #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
544 pub fn validate_custom(s: &str) -> bool {
545 Self::parse(s).is_ok()
546 }
547
548 #[inline]
557 pub fn as_str(&self) -> &str {
558 &self.0
559 }
560
561 pub fn is_well_known(&self) -> bool {
563 matches!(
564 self.0.as_str(),
565 SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
566 )
567 }
568
569 pub fn namespace(&self) -> Option<&str> {
571 self.0.split(':').next().filter(|_| self.0.contains(':'))
572 }
573}
574
575impl fmt::Display for Capability {
576 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577 f.write_str(&self.0)
578 }
579}
580
581impl TryFrom<String> for Capability {
582 type Error = CapabilityError;
583
584 fn try_from(s: String) -> Result<Self, Self::Error> {
585 let canonical = s.trim().to_lowercase();
586
587 if canonical.is_empty() {
588 return Err(CapabilityError::Empty);
589 }
590 if canonical.len() > Self::MAX_LEN {
591 return Err(CapabilityError::TooLong(canonical.len()));
592 }
593 if !canonical
594 .chars()
595 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
596 {
597 return Err(CapabilityError::InvalidChars(canonical));
598 }
599
600 Ok(Self(canonical))
603 }
604}
605
606impl std::str::FromStr for Capability {
607 type Err = CapabilityError;
608
609 fn from_str(s: &str) -> Result<Self, Self::Err> {
628 let normalized = s.trim().to_lowercase().replace('-', "_");
629 match normalized.as_str() {
630 "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
631 "sign_release" | "signrelease" => Ok(Capability::sign_release()),
632 "manage_members" | "managemembers" => Ok(Capability::manage_members()),
633 "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
634 _ => Capability::parse(&normalized),
635 }
636 }
637}
638
639impl From<Capability> for String {
640 fn from(cap: Capability) -> Self {
641 cap.0
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
651pub struct IdentityBundle {
652 pub identity_did: String,
654 pub public_key_hex: String,
656 pub attestation_chain: Vec<Attestation>,
658 pub bundle_timestamp: DateTime<Utc>,
660 pub max_valid_for_secs: u64,
662}
663
664impl IdentityBundle {
665 pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
675 let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
676 if age > self.max_valid_for_secs {
677 return Err(AttestationError::BundleExpired {
678 age_secs: age,
679 max_secs: self.max_valid_for_secs,
680 });
681 }
682 Ok(())
683 }
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
688#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
689pub struct Attestation {
690 pub version: u32,
692 pub rid: ResourceId,
694 pub issuer: IdentityDID,
696 pub subject: DeviceDID,
698 pub device_public_key: Ed25519PublicKey,
700 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
702 pub identity_signature: Ed25519Signature,
703 pub device_signature: Ed25519Signature,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub revoked_at: Option<DateTime<Utc>>,
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub expires_at: Option<DateTime<Utc>>,
711 pub timestamp: Option<DateTime<Utc>>,
713 #[serde(skip_serializing_if = "Option::is_none")]
715 pub note: Option<String>,
716 #[serde(skip_serializing_if = "Option::is_none")]
718 pub payload: Option<Value>,
719
720 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub role: Option<Role>,
723
724 #[serde(default, skip_serializing_if = "Vec::is_empty")]
726 pub capabilities: Vec<Capability>,
727
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub delegated_by: Option<IdentityDID>,
731
732 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub signer_type: Option<SignerType>,
736}
737
738#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
743#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
744pub enum SignerType {
745 Human,
747 Agent,
749 Workload,
751}
752
753#[derive(Debug, Clone, Serialize)]
763pub struct VerifiedAttestation(Attestation);
764
765impl VerifiedAttestation {
766 pub fn inner(&self) -> &Attestation {
768 &self.0
769 }
770
771 pub fn into_inner(self) -> Attestation {
773 self.0
774 }
775
776 #[doc(hidden)]
782 pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
783 Self(attestation)
784 }
785
786 pub(crate) fn from_verified(attestation: Attestation) -> Self {
787 Self(attestation)
788 }
789}
790
791impl std::ops::Deref for VerifiedAttestation {
792 type Target = Attestation;
793
794 fn deref(&self) -> &Attestation {
795 &self.0
796 }
797}
798
799#[derive(Serialize, Debug)]
801pub struct CanonicalAttestationData<'a> {
802 pub version: u32,
804 pub rid: &'a str,
806 pub issuer: &'a IdentityDID,
808 pub subject: &'a DeviceDID,
810 #[serde(with = "hex::serde")]
812 pub device_public_key: &'a [u8],
813 pub payload: &'a Option<Value>,
815 pub timestamp: &'a Option<DateTime<Utc>>,
817 pub expires_at: &'a Option<DateTime<Utc>>,
819 pub revoked_at: &'a Option<DateTime<Utc>>,
821 pub note: &'a Option<String>,
823
824 #[serde(skip_serializing_if = "Option::is_none")]
826 pub role: Option<&'a str>,
827 #[serde(skip_serializing_if = "Option::is_none")]
829 pub capabilities: Option<&'a Vec<Capability>>,
830 #[serde(skip_serializing_if = "Option::is_none")]
832 pub delegated_by: Option<&'a IdentityDID>,
833 #[serde(skip_serializing_if = "Option::is_none")]
835 pub signer_type: Option<&'a SignerType>,
836}
837
838pub fn canonicalize_attestation_data(
843 data: &CanonicalAttestationData,
844) -> Result<Vec<u8>, AttestationError> {
845 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
846 AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
847 })?;
848 debug!(
849 "Generated canonical data (standard): {}",
850 canonical_json_string
851 );
852 Ok(canonical_json_string.into_bytes())
853}
854
855impl Attestation {
856 pub fn is_revoked(&self) -> bool {
858 self.revoked_at.is_some()
859 }
860
861 pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
865 if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
866 return Err(AttestationError::InputTooLarge(format!(
867 "attestation JSON is {} bytes, max {}",
868 json_bytes.len(),
869 MAX_ATTESTATION_JSON_SIZE
870 )));
871 }
872 serde_json::from_slice(json_bytes)
873 .map_err(|e| AttestationError::SerializationError(e.to_string()))
874 }
875
876 pub fn to_debug_string(&self) -> String {
878 format!(
879 "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
880 self.rid,
881 self.issuer,
882 self.subject, hex::encode(self.device_public_key.as_bytes()),
884 hex::encode(self.identity_signature.as_bytes()),
885 hex::encode(self.device_signature.as_bytes()),
886 self.revoked_at,
887 self.expires_at,
888 self.note
889 )
890 }
891}
892
893#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
960#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
961pub struct ThresholdPolicy {
962 pub threshold: u8,
964
965 pub signers: Vec<String>,
967
968 pub policy_id: String,
970
971 #[serde(default, skip_serializing_if = "Option::is_none")]
973 pub scope: Option<Capability>,
974
975 #[serde(default, skip_serializing_if = "Option::is_none")]
977 pub ceremony_endpoint: Option<String>,
978}
979
980impl ThresholdPolicy {
981 pub fn new(threshold: u8, signers: Vec<String>, policy_id: String) -> Self {
983 Self {
984 threshold,
985 signers,
986 policy_id,
987 scope: None,
988 ceremony_endpoint: None,
989 }
990 }
991
992 pub fn is_valid(&self) -> bool {
994 if self.threshold < 1 {
996 return false;
997 }
998 if self.threshold as usize > self.signers.len() {
1000 return false;
1001 }
1002 if self.signers.is_empty() {
1004 return false;
1005 }
1006 if self.policy_id.is_empty() {
1008 return false;
1009 }
1010 true
1011 }
1012
1013 pub fn m_of_n(&self) -> (u8, usize) {
1015 (self.threshold, self.signers.len())
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 #[test]
1028 fn capability_serializes_to_snake_case() {
1029 assert_eq!(
1030 serde_json::to_string(&Capability::sign_commit()).unwrap(),
1031 r#""sign_commit""#
1032 );
1033 assert_eq!(
1034 serde_json::to_string(&Capability::sign_release()).unwrap(),
1035 r#""sign_release""#
1036 );
1037 assert_eq!(
1038 serde_json::to_string(&Capability::manage_members()).unwrap(),
1039 r#""manage_members""#
1040 );
1041 assert_eq!(
1042 serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1043 r#""rotate_keys""#
1044 );
1045 }
1046
1047 #[test]
1048 fn capability_deserializes_from_snake_case() {
1049 assert_eq!(
1050 serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1051 Capability::sign_commit()
1052 );
1053 assert_eq!(
1054 serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1055 Capability::sign_release()
1056 );
1057 assert_eq!(
1058 serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1059 Capability::manage_members()
1060 );
1061 assert_eq!(
1062 serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1063 Capability::rotate_keys()
1064 );
1065 }
1066
1067 #[test]
1068 fn capability_custom_serializes_as_string() {
1069 let cap = Capability::parse("acme:deploy").unwrap();
1070 assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1071 }
1072
1073 #[test]
1074 fn capability_custom_deserializes_unknown_strings() {
1075 let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1077 assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1078 }
1079
1080 #[test]
1085 fn capability_parse_accepts_valid_strings() {
1086 assert!(Capability::parse("deploy").is_ok());
1087 assert!(Capability::parse("acme:deploy").is_ok());
1088 assert!(Capability::parse("my-custom-cap").is_ok());
1089 assert!(Capability::parse("org:team:action").is_ok());
1090 assert!(Capability::parse("with_underscore").is_ok()); }
1092
1093 #[test]
1094 fn capability_parse_rejects_invalid_strings() {
1095 assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
1097
1098 assert!(matches!(
1100 Capability::parse(&"a".repeat(65)),
1101 Err(CapabilityError::TooLong(65))
1102 ));
1103
1104 assert!(matches!(
1106 Capability::parse("has spaces"),
1107 Err(CapabilityError::InvalidChars(_))
1108 ));
1109 assert!(matches!(
1110 Capability::parse("has.dot"),
1111 Err(CapabilityError::InvalidChars(_))
1112 ));
1113 }
1114
1115 #[test]
1116 fn capability_parse_rejects_reserved_namespace() {
1117 assert!(matches!(
1118 Capability::parse("auths:custom"),
1119 Err(CapabilityError::ReservedNamespace)
1120 ));
1121 assert!(matches!(
1122 Capability::parse("auths:sign_commit"),
1123 Err(CapabilityError::ReservedNamespace)
1124 ));
1125 }
1126
1127 #[test]
1128 fn capability_parse_normalizes_to_lowercase() {
1129 let cap = Capability::parse("DEPLOY").unwrap();
1130 assert_eq!(cap.as_str(), "deploy");
1131
1132 let cap = Capability::parse("ACME:Deploy").unwrap();
1133 assert_eq!(cap.as_str(), "acme:deploy");
1134 }
1135
1136 #[test]
1137 fn capability_parse_trims_whitespace() {
1138 let cap = Capability::parse(" deploy ").unwrap();
1139 assert_eq!(cap.as_str(), "deploy");
1140 }
1141
1142 #[test]
1147 fn capability_is_hashable() {
1148 use std::collections::HashSet;
1149 let mut set = HashSet::new();
1150 set.insert(Capability::sign_commit());
1151 set.insert(Capability::sign_release());
1152 set.insert(Capability::parse("test").unwrap());
1153 assert_eq!(set.len(), 3);
1154 assert!(set.contains(&Capability::sign_commit()));
1155 }
1156
1157 #[test]
1158 fn capability_equality_with_different_construction_paths() {
1159 let from_constructor = Capability::sign_commit();
1161 let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
1162 assert_eq!(from_constructor, from_deser);
1163
1164 let from_parse = Capability::parse("acme:deploy").unwrap();
1166 let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
1167 assert_eq!(from_parse, from_deser);
1168 }
1169
1170 #[test]
1175 fn capability_display_matches_canonical_form() {
1176 assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
1177 assert_eq!(Capability::sign_release().to_string(), "sign_release");
1178 assert_eq!(Capability::manage_members().to_string(), "manage_members");
1179 assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
1180 assert_eq!(
1181 Capability::parse("acme:deploy").unwrap().to_string(),
1182 "acme:deploy"
1183 );
1184 }
1185
1186 #[test]
1187 fn capability_as_str_returns_canonical_form() {
1188 assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
1189 assert_eq!(Capability::sign_release().as_str(), "sign_release");
1190 assert_eq!(Capability::manage_members().as_str(), "manage_members");
1191 assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
1192 assert_eq!(
1193 Capability::parse("acme:deploy").unwrap().as_str(),
1194 "acme:deploy"
1195 );
1196 }
1197
1198 #[test]
1199 fn capability_is_well_known() {
1200 assert!(Capability::sign_commit().is_well_known());
1201 assert!(Capability::sign_release().is_well_known());
1202 assert!(Capability::manage_members().is_well_known());
1203 assert!(Capability::rotate_keys().is_well_known());
1204 assert!(!Capability::parse("custom").unwrap().is_well_known());
1205 }
1206
1207 #[test]
1208 fn capability_namespace() {
1209 assert_eq!(
1210 Capability::parse("acme:deploy").unwrap().namespace(),
1211 Some("acme")
1212 );
1213 assert_eq!(
1214 Capability::parse("org:team:action").unwrap().namespace(),
1215 Some("org")
1216 );
1217 assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
1218 }
1219
1220 #[test]
1225 fn capability_vec_serializes_as_array() {
1226 let caps = vec![Capability::sign_commit(), Capability::sign_release()];
1227 let json = serde_json::to_string(&caps).unwrap();
1228 assert_eq!(json, r#"["sign_commit","sign_release"]"#);
1229 }
1230
1231 #[test]
1232 fn capability_vec_deserializes_from_array() {
1233 let json = r#"["sign_commit","manage_members","custom-cap"]"#;
1234 let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
1235 assert_eq!(caps.len(), 3);
1236 assert_eq!(caps[0], Capability::sign_commit());
1237 assert_eq!(caps[1], Capability::manage_members());
1238 assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
1239 }
1240
1241 #[test]
1246 fn capability_serde_roundtrip_well_known() {
1247 let caps = vec![
1248 Capability::sign_commit(),
1249 Capability::sign_release(),
1250 Capability::manage_members(),
1251 Capability::rotate_keys(),
1252 ];
1253 for cap in caps {
1254 let json = serde_json::to_string(&cap).unwrap();
1255 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1256 assert_eq!(cap, roundtrip);
1257 }
1258 }
1259
1260 #[test]
1261 fn capability_serde_roundtrip_custom() {
1262 let caps = vec![
1263 Capability::parse("deploy").unwrap(),
1264 Capability::parse("acme:deploy").unwrap(),
1265 Capability::parse("org:team:action").unwrap(),
1266 ];
1267 for cap in caps {
1268 let json = serde_json::to_string(&cap).unwrap();
1269 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1270 assert_eq!(cap, roundtrip);
1271 }
1272 }
1273
1274 #[test]
1277 fn attestation_old_json_without_org_fields_deserializes() {
1278 let old_json = r#"{
1280 "version": 1,
1281 "rid": "test-rid",
1282 "issuer": "did:key:issuer",
1283 "subject": "did:key:subject",
1284 "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
1285 "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1286 "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1287 "revoked_at": null,
1288 "timestamp": null
1289 }"#;
1290
1291 let att: Attestation = serde_json::from_str(old_json).unwrap();
1292
1293 assert_eq!(att.role, None);
1295 assert!(att.capabilities.is_empty());
1296 assert_eq!(att.delegated_by, None);
1297 }
1298
1299 #[test]
1300 fn attestation_with_org_fields_serializes_correctly() {
1301 use crate::types::DeviceDID;
1302
1303 let att = Attestation {
1304 version: 1,
1305 rid: ResourceId::new("test-rid"),
1306 issuer: IdentityDID::new("did:key:issuer"),
1307 subject: DeviceDID::new("did:key:subject".to_string()),
1308 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1309 identity_signature: Ed25519Signature::empty(),
1310 device_signature: Ed25519Signature::empty(),
1311 revoked_at: None,
1312 expires_at: None,
1313 timestamp: None,
1314 note: None,
1315 payload: None,
1316 role: Some(Role::Admin),
1317 capabilities: vec![Capability::sign_commit(), Capability::manage_members()],
1318 delegated_by: Some(IdentityDID::new("did:key:delegator")),
1319 signer_type: None,
1320 };
1321
1322 let json = serde_json::to_string(&att).unwrap();
1323 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1324
1325 assert_eq!(parsed["role"], "admin");
1326 assert_eq!(parsed["capabilities"][0], "sign_commit");
1327 assert_eq!(parsed["capabilities"][1], "manage_members");
1328 assert_eq!(parsed["delegated_by"], "did:key:delegator");
1329 }
1330
1331 #[test]
1332 fn attestation_without_org_fields_omits_them_in_json() {
1333 use crate::types::DeviceDID;
1334
1335 let att = Attestation {
1336 version: 1,
1337 rid: ResourceId::new("test-rid"),
1338 issuer: IdentityDID::new("did:key:issuer"),
1339 subject: DeviceDID::new("did:key:subject".to_string()),
1340 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1341 identity_signature: Ed25519Signature::empty(),
1342 device_signature: Ed25519Signature::empty(),
1343 revoked_at: None,
1344 expires_at: None,
1345 timestamp: None,
1346 note: None,
1347 payload: None,
1348 role: None,
1349 capabilities: vec![],
1350 delegated_by: None,
1351 signer_type: None,
1352 };
1353
1354 let json = serde_json::to_string(&att).unwrap();
1355 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1356
1357 assert!(parsed.get("role").is_none());
1359 assert!(parsed.get("capabilities").is_none());
1360 assert!(parsed.get("delegated_by").is_none());
1361 }
1362
1363 #[test]
1364 fn attestation_with_org_fields_roundtrips() {
1365 use crate::types::DeviceDID;
1366
1367 let original = Attestation {
1368 version: 1,
1369 rid: ResourceId::new("test-rid"),
1370 issuer: IdentityDID::new("did:key:issuer"),
1371 subject: DeviceDID::new("did:key:subject".to_string()),
1372 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1373 identity_signature: Ed25519Signature::empty(),
1374 device_signature: Ed25519Signature::empty(),
1375 revoked_at: None,
1376 expires_at: None,
1377 timestamp: None,
1378 note: None,
1379 payload: None,
1380 role: Some(Role::Member),
1381 capabilities: vec![Capability::sign_commit(), Capability::sign_release()],
1382 delegated_by: Some(IdentityDID::new("did:key:admin")),
1383 signer_type: None,
1384 };
1385
1386 let json = serde_json::to_string(&original).unwrap();
1387 let deserialized: Attestation = serde_json::from_str(&json).unwrap();
1388
1389 assert_eq!(original.role, deserialized.role);
1390 assert_eq!(original.capabilities, deserialized.capabilities);
1391 assert_eq!(original.delegated_by, deserialized.delegated_by);
1392 }
1393
1394 #[test]
1397 fn threshold_policy_new_creates_valid_policy() {
1398 let policy = ThresholdPolicy::new(
1399 2,
1400 vec![
1401 "did:key:alice".to_string(),
1402 "did:key:bob".to_string(),
1403 "did:key:carol".to_string(),
1404 ],
1405 "test-policy".to_string(),
1406 );
1407
1408 assert_eq!(policy.threshold, 2);
1409 assert_eq!(policy.signers.len(), 3);
1410 assert_eq!(policy.policy_id, "test-policy");
1411 assert!(policy.scope.is_none());
1412 assert!(policy.ceremony_endpoint.is_none());
1413 }
1414
1415 #[test]
1416 fn threshold_policy_is_valid_checks_constraints() {
1417 let valid = ThresholdPolicy::new(
1419 2,
1420 vec!["a".to_string(), "b".to_string(), "c".to_string()],
1421 "policy".to_string(),
1422 );
1423 assert!(valid.is_valid());
1424
1425 let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
1427 assert!(!zero_threshold.is_valid());
1428
1429 let too_high = ThresholdPolicy::new(
1431 3,
1432 vec!["a".to_string(), "b".to_string()],
1433 "policy".to_string(),
1434 );
1435 assert!(!too_high.is_valid());
1436
1437 let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
1439 assert!(!no_signers.is_valid());
1440
1441 let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
1443 assert!(!no_id.is_valid());
1444 }
1445
1446 #[test]
1447 fn threshold_policy_m_of_n_returns_correct_values() {
1448 let policy = ThresholdPolicy::new(
1449 2,
1450 vec!["a".to_string(), "b".to_string(), "c".to_string()],
1451 "policy".to_string(),
1452 );
1453 let (m, n) = policy.m_of_n();
1454 assert_eq!(m, 2);
1455 assert_eq!(n, 3);
1456 }
1457
1458 #[test]
1459 fn threshold_policy_serializes_correctly() {
1460 let mut policy = ThresholdPolicy::new(
1461 2,
1462 vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
1463 "release-policy".to_string(),
1464 );
1465 policy.scope = Some(Capability::sign_release());
1466 policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
1467
1468 let json = serde_json::to_string(&policy).unwrap();
1469 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1470
1471 assert_eq!(parsed["threshold"], 2);
1472 assert_eq!(parsed["signers"][0], "did:key:alice");
1473 assert_eq!(parsed["policy_id"], "release-policy");
1474 assert_eq!(parsed["scope"], "sign_release");
1475 assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
1476 }
1477
1478 #[test]
1479 fn threshold_policy_without_optional_fields_omits_them() {
1480 let policy =
1481 ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
1482
1483 let json = serde_json::to_string(&policy).unwrap();
1484 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1485
1486 assert!(parsed.get("scope").is_none());
1487 assert!(parsed.get("ceremony_endpoint").is_none());
1488 }
1489
1490 #[test]
1491 fn threshold_policy_roundtrips() {
1492 let mut original = ThresholdPolicy::new(
1493 3,
1494 vec![
1495 "a".to_string(),
1496 "b".to_string(),
1497 "c".to_string(),
1498 "d".to_string(),
1499 ],
1500 "important-policy".to_string(),
1501 );
1502 original.scope = Some(Capability::rotate_keys());
1503
1504 let json = serde_json::to_string(&original).unwrap();
1505 let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
1506
1507 assert_eq!(original, deserialized);
1508 }
1509
1510 #[test]
1513 fn identity_bundle_serializes_correctly() {
1514 let bundle = IdentityBundle {
1515 identity_did: "did:keri:test123".to_string(),
1516 public_key_hex: "aabbccdd".to_string(),
1517 attestation_chain: vec![],
1518 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1519 .unwrap()
1520 .with_timezone(&Utc),
1521 max_valid_for_secs: 86400,
1522 };
1523
1524 let json = serde_json::to_string(&bundle).unwrap();
1525 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1526
1527 assert_eq!(parsed["identity_did"], "did:keri:test123");
1528 assert_eq!(parsed["public_key_hex"], "aabbccdd");
1529 assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
1530 }
1531
1532 #[test]
1533 fn identity_bundle_deserializes_correctly() {
1534 let json = r#"{
1535 "identity_did": "did:keri:abc123",
1536 "public_key_hex": "112233",
1537 "attestation_chain": [],
1538 "bundle_timestamp": "2099-01-01T00:00:00Z",
1539 "max_valid_for_secs": 86400
1540 }"#;
1541
1542 let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
1543
1544 assert_eq!(bundle.identity_did, "did:keri:abc123");
1545 assert_eq!(bundle.public_key_hex, "112233");
1546 assert!(bundle.attestation_chain.is_empty());
1547 }
1548
1549 #[test]
1550 fn identity_bundle_roundtrips() {
1551 use crate::types::DeviceDID;
1552
1553 let attestation = Attestation {
1554 version: 1,
1555 rid: ResourceId::new("test-rid"),
1556 issuer: IdentityDID::new("did:key:issuer"),
1557 subject: DeviceDID::new("did:key:subject".to_string()),
1558 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1559 identity_signature: Ed25519Signature::empty(),
1560 device_signature: Ed25519Signature::empty(),
1561 revoked_at: None,
1562 expires_at: None,
1563 timestamp: None,
1564 note: None,
1565 payload: None,
1566 role: None,
1567 capabilities: vec![],
1568 delegated_by: None,
1569 signer_type: None,
1570 };
1571
1572 let original = IdentityBundle {
1573 identity_did: "did:keri:example".to_string(),
1574 public_key_hex: "deadbeef".to_string(),
1575 attestation_chain: vec![attestation],
1576 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1577 .unwrap()
1578 .with_timezone(&Utc),
1579 max_valid_for_secs: 86400,
1580 };
1581
1582 let json = serde_json::to_string(&original).unwrap();
1583 let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
1584
1585 assert_eq!(original.identity_did, deserialized.identity_did);
1586 assert_eq!(original.public_key_hex, deserialized.public_key_hex);
1587 assert_eq!(
1588 original.attestation_chain.len(),
1589 deserialized.attestation_chain.len()
1590 );
1591 }
1592}