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
21const SIGN_COMMIT: &str = "sign_commit";
23const SIGN_RELEASE: &str = "sign_release";
24const MANAGE_MEMBERS: &str = "manage_members";
25const ROTATE_KEYS: &str = "rotate_keys";
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(transparent)]
38pub struct ResourceId(String);
39
40impl ResourceId {
41 pub fn new(s: impl Into<String>) -> Self {
43 Self(s.into())
44 }
45
46 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl Deref for ResourceId {
53 type Target = str;
54 fn deref(&self) -> &str {
55 &self.0
56 }
57}
58
59impl fmt::Display for ResourceId {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(&self.0)
62 }
63}
64
65impl From<String> for ResourceId {
66 fn from(s: String) -> Self {
67 Self(s)
68 }
69}
70
71impl From<&str> for ResourceId {
72 fn from(s: &str) -> Self {
73 Self(s.to_string())
74 }
75}
76
77impl PartialEq<str> for ResourceId {
78 fn eq(&self, other: &str) -> bool {
79 self.0 == other
80 }
81}
82
83impl PartialEq<&str> for ResourceId {
84 fn eq(&self, other: &&str) -> bool {
85 self.0 == *other
86 }
87}
88
89impl PartialEq<String> for ResourceId {
90 fn eq(&self, other: &String) -> bool {
91 self.0 == *other
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum Role {
106 Admin,
108 Member,
110 Readonly,
112}
113
114impl Role {
115 pub fn as_str(&self) -> &str {
117 match self {
118 Role::Admin => "admin",
119 Role::Member => "member",
120 Role::Readonly => "readonly",
121 }
122 }
123
124 pub fn default_capabilities(&self) -> Vec<Capability> {
126 match self {
127 Role::Admin => vec![
128 Capability::sign_commit(),
129 Capability::sign_release(),
130 Capability::manage_members(),
131 Capability::rotate_keys(),
132 ],
133 Role::Member => vec![Capability::sign_commit(), Capability::sign_release()],
134 Role::Readonly => vec![],
135 }
136 }
137}
138
139impl fmt::Display for Role {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 f.write_str(self.as_str())
142 }
143}
144
145impl FromStr for Role {
146 type Err = RoleParseError;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 match s.trim().to_lowercase().as_str() {
150 "admin" => Ok(Role::Admin),
151 "member" => Ok(Role::Member),
152 "readonly" => Ok(Role::Readonly),
153 other => Err(RoleParseError(other.to_string())),
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
160#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
161pub struct RoleParseError(String);
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub struct Ed25519PublicKey([u8; 32]);
173
174impl Ed25519PublicKey {
175 pub fn from_bytes(bytes: [u8; 32]) -> Self {
177 Self(bytes)
178 }
179
180 pub fn try_from_slice(slice: &[u8]) -> Result<Self, Ed25519KeyError> {
190 let arr: [u8; 32] = slice
191 .try_into()
192 .map_err(|_| Ed25519KeyError::InvalidLength(slice.len()))?;
193 Ok(Self(arr))
194 }
195
196 pub fn as_bytes(&self) -> &[u8; 32] {
198 &self.0
199 }
200
201 pub fn is_zero(&self) -> bool {
203 self.0 == [0u8; 32]
204 }
205}
206
207impl Serialize for Ed25519PublicKey {
208 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
209 s.serialize_str(&hex::encode(self.0))
210 }
211}
212
213impl<'de> Deserialize<'de> for Ed25519PublicKey {
214 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
215 let s = String::deserialize(d)?;
216 let bytes =
217 hex::decode(&s).map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
218 let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
219 serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len()))
220 })?;
221 Ok(Self(arr))
222 }
223}
224
225impl fmt::Display for Ed25519PublicKey {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 f.write_str(&hex::encode(self.0))
228 }
229}
230
231impl AsRef<[u8]> for Ed25519PublicKey {
232 fn as_ref(&self) -> &[u8] {
233 &self.0
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
239pub enum Ed25519KeyError {
240 #[error("expected 32 bytes, got {0}")]
242 InvalidLength(usize),
243 #[error("invalid hex: {0}")]
245 InvalidHex(String),
246}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct Ed25519Signature([u8; 64]);
255
256impl Ed25519Signature {
257 pub fn from_bytes(bytes: [u8; 64]) -> Self {
259 Self(bytes)
260 }
261
262 pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
264 let arr: [u8; 64] = slice
265 .try_into()
266 .map_err(|_| SignatureLengthError(slice.len()))?;
267 Ok(Self(arr))
268 }
269
270 pub fn empty() -> Self {
272 Self([0u8; 64])
273 }
274
275 pub fn is_empty(&self) -> bool {
277 self.0 == [0u8; 64]
278 }
279
280 pub fn as_bytes(&self) -> &[u8; 64] {
282 &self.0
283 }
284}
285
286impl Default for Ed25519Signature {
287 fn default() -> Self {
288 Self::empty()
289 }
290}
291
292impl std::fmt::Display for Ed25519Signature {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 write!(f, "{}", hex::encode(self.0))
295 }
296}
297
298impl AsRef<[u8]> for Ed25519Signature {
299 fn as_ref(&self) -> &[u8] {
300 &self.0
301 }
302}
303
304impl serde::Serialize for Ed25519Signature {
305 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
306 serializer.serialize_str(&hex::encode(self.0))
307 }
308}
309
310impl<'de> serde::Deserialize<'de> for Ed25519Signature {
311 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
312 let s = String::deserialize(deserializer)?;
313 if s.is_empty() {
314 return Ok(Self::empty());
315 }
316 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
317 Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
323#[error("expected 64 bytes, got {0}")]
324pub struct SignatureLengthError(pub usize);
325
326#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
332pub enum CapabilityError {
333 #[error("capability is empty")]
335 Empty,
336 #[error("capability exceeds 64 chars: {0}")]
338 TooLong(usize),
339 #[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
341 InvalidChars(String),
342 #[error(
344 "reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
345 )]
346 ReservedNamespace,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
377#[serde(try_from = "String", into = "String")]
378pub struct Capability(String);
379
380impl Capability {
381 pub const MAX_LEN: usize = 64;
383
384 const RESERVED_PREFIX: &'static str = "auths:";
386
387 #[inline]
395 pub fn sign_commit() -> Self {
396 Self(SIGN_COMMIT.to_string())
397 }
398
399 #[inline]
403 pub fn sign_release() -> Self {
404 Self(SIGN_RELEASE.to_string())
405 }
406
407 #[inline]
411 pub fn manage_members() -> Self {
412 Self(MANAGE_MEMBERS.to_string())
413 }
414
415 #[inline]
419 pub fn rotate_keys() -> Self {
420 Self(ROTATE_KEYS.to_string())
421 }
422
423 pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
455 let canonical = raw.trim().to_lowercase();
456
457 if canonical.is_empty() {
458 return Err(CapabilityError::Empty);
459 }
460 if canonical.len() > Self::MAX_LEN {
461 return Err(CapabilityError::TooLong(canonical.len()));
462 }
463 if !canonical
464 .chars()
465 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
466 {
467 return Err(CapabilityError::InvalidChars(canonical));
468 }
469 if canonical.starts_with(Self::RESERVED_PREFIX) {
470 return Err(CapabilityError::ReservedNamespace);
471 }
472
473 Ok(Self(canonical))
474 }
475
476 #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
484 pub fn custom(s: impl Into<String>) -> Option<Self> {
485 Self::parse(&s.into()).ok()
486 }
487
488 #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
494 pub fn validate_custom(s: &str) -> bool {
495 Self::parse(s).is_ok()
496 }
497
498 #[inline]
507 pub fn as_str(&self) -> &str {
508 &self.0
509 }
510
511 pub fn is_well_known(&self) -> bool {
513 matches!(
514 self.0.as_str(),
515 SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
516 )
517 }
518
519 pub fn namespace(&self) -> Option<&str> {
521 self.0.split(':').next().filter(|_| self.0.contains(':'))
522 }
523}
524
525impl fmt::Display for Capability {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 f.write_str(&self.0)
528 }
529}
530
531impl TryFrom<String> for Capability {
532 type Error = CapabilityError;
533
534 fn try_from(s: String) -> Result<Self, Self::Error> {
535 let canonical = s.trim().to_lowercase();
536
537 if canonical.is_empty() {
538 return Err(CapabilityError::Empty);
539 }
540 if canonical.len() > Self::MAX_LEN {
541 return Err(CapabilityError::TooLong(canonical.len()));
542 }
543 if !canonical
544 .chars()
545 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
546 {
547 return Err(CapabilityError::InvalidChars(canonical));
548 }
549
550 Ok(Self(canonical))
553 }
554}
555
556impl std::str::FromStr for Capability {
557 type Err = CapabilityError;
558
559 fn from_str(s: &str) -> Result<Self, Self::Err> {
578 let normalized = s.trim().to_lowercase().replace('-', "_");
579 match normalized.as_str() {
580 "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
581 "sign_release" | "signrelease" => Ok(Capability::sign_release()),
582 "manage_members" | "managemembers" => Ok(Capability::manage_members()),
583 "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
584 _ => Capability::parse(&normalized),
585 }
586 }
587}
588
589impl From<Capability> for String {
590 fn from(cap: Capability) -> Self {
591 cap.0
592 }
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
600pub struct IdentityBundle {
601 pub identity_did: String,
603 pub public_key_hex: String,
605 pub attestation_chain: Vec<Attestation>,
607 pub bundle_timestamp: DateTime<Utc>,
609 pub max_valid_for_secs: u64,
611}
612
613impl IdentityBundle {
614 pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
624 let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
625 if age > self.max_valid_for_secs {
626 return Err(AttestationError::BundleExpired {
627 age_secs: age,
628 max_secs: self.max_valid_for_secs,
629 });
630 }
631 Ok(())
632 }
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
637pub struct Attestation {
638 pub version: u32,
640 pub rid: ResourceId,
642 pub issuer: IdentityDID,
644 pub subject: DeviceDID,
646 pub device_public_key: Ed25519PublicKey,
648 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
650 pub identity_signature: Ed25519Signature,
651 pub device_signature: Ed25519Signature,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub revoked_at: Option<DateTime<Utc>>,
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub expires_at: Option<DateTime<Utc>>,
659 pub timestamp: Option<DateTime<Utc>>,
661 #[serde(skip_serializing_if = "Option::is_none")]
663 pub note: Option<String>,
664 #[serde(skip_serializing_if = "Option::is_none")]
666 pub payload: Option<Value>,
667
668 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub role: Option<Role>,
671
672 #[serde(default, skip_serializing_if = "Vec::is_empty")]
674 pub capabilities: Vec<Capability>,
675
676 #[serde(default, skip_serializing_if = "Option::is_none")]
678 pub delegated_by: Option<IdentityDID>,
679
680 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub signer_type: Option<SignerType>,
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
691pub enum SignerType {
692 Human,
694 Agent,
696 Workload,
698}
699
700#[derive(Debug, Clone, Serialize)]
710pub struct VerifiedAttestation(Attestation);
711
712impl VerifiedAttestation {
713 pub fn inner(&self) -> &Attestation {
715 &self.0
716 }
717
718 pub fn into_inner(self) -> Attestation {
720 self.0
721 }
722
723 #[doc(hidden)]
729 pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
730 Self(attestation)
731 }
732
733 pub(crate) fn from_verified(attestation: Attestation) -> Self {
734 Self(attestation)
735 }
736}
737
738impl std::ops::Deref for VerifiedAttestation {
739 type Target = Attestation;
740
741 fn deref(&self) -> &Attestation {
742 &self.0
743 }
744}
745
746#[derive(Serialize, Debug)]
748pub struct CanonicalAttestationData<'a> {
749 pub version: u32,
751 pub rid: &'a str,
753 pub issuer: &'a IdentityDID,
755 pub subject: &'a DeviceDID,
757 #[serde(with = "hex::serde")]
759 pub device_public_key: &'a [u8],
760 pub payload: &'a Option<Value>,
762 pub timestamp: &'a Option<DateTime<Utc>>,
764 pub expires_at: &'a Option<DateTime<Utc>>,
766 pub revoked_at: &'a Option<DateTime<Utc>>,
768 pub note: &'a Option<String>,
770
771 #[serde(skip_serializing_if = "Option::is_none")]
773 pub role: Option<&'a str>,
774 #[serde(skip_serializing_if = "Option::is_none")]
776 pub capabilities: Option<&'a Vec<Capability>>,
777 #[serde(skip_serializing_if = "Option::is_none")]
779 pub delegated_by: Option<&'a IdentityDID>,
780 #[serde(skip_serializing_if = "Option::is_none")]
782 pub signer_type: Option<&'a SignerType>,
783}
784
785pub fn canonicalize_attestation_data(
790 data: &CanonicalAttestationData,
791) -> Result<Vec<u8>, AttestationError> {
792 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
793 AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
794 })?;
795 debug!(
796 "Generated canonical data (standard): {}",
797 canonical_json_string
798 );
799 Ok(canonical_json_string.into_bytes())
800}
801
802impl Attestation {
803 pub fn is_revoked(&self) -> bool {
805 self.revoked_at.is_some()
806 }
807
808 pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
812 if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
813 return Err(AttestationError::InputTooLarge(format!(
814 "attestation JSON is {} bytes, max {}",
815 json_bytes.len(),
816 MAX_ATTESTATION_JSON_SIZE
817 )));
818 }
819 serde_json::from_slice(json_bytes)
820 .map_err(|e| AttestationError::SerializationError(e.to_string()))
821 }
822
823 pub fn to_debug_string(&self) -> String {
825 format!(
826 "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
827 self.rid,
828 self.issuer,
829 self.subject, hex::encode(self.device_public_key.as_bytes()),
831 hex::encode(self.identity_signature.as_bytes()),
832 hex::encode(self.device_signature.as_bytes()),
833 self.revoked_at,
834 self.expires_at,
835 self.note
836 )
837 }
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
907pub struct ThresholdPolicy {
908 pub threshold: u8,
910
911 pub signers: Vec<String>,
913
914 pub policy_id: String,
916
917 #[serde(default, skip_serializing_if = "Option::is_none")]
919 pub scope: Option<Capability>,
920
921 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub ceremony_endpoint: Option<String>,
924}
925
926impl ThresholdPolicy {
927 pub fn new(threshold: u8, signers: Vec<String>, policy_id: String) -> Self {
929 Self {
930 threshold,
931 signers,
932 policy_id,
933 scope: None,
934 ceremony_endpoint: None,
935 }
936 }
937
938 pub fn is_valid(&self) -> bool {
940 if self.threshold < 1 {
942 return false;
943 }
944 if self.threshold as usize > self.signers.len() {
946 return false;
947 }
948 if self.signers.is_empty() {
950 return false;
951 }
952 if self.policy_id.is_empty() {
954 return false;
955 }
956 true
957 }
958
959 pub fn m_of_n(&self) -> (u8, usize) {
961 (self.threshold, self.signers.len())
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968
969 #[test]
974 fn capability_serializes_to_snake_case() {
975 assert_eq!(
976 serde_json::to_string(&Capability::sign_commit()).unwrap(),
977 r#""sign_commit""#
978 );
979 assert_eq!(
980 serde_json::to_string(&Capability::sign_release()).unwrap(),
981 r#""sign_release""#
982 );
983 assert_eq!(
984 serde_json::to_string(&Capability::manage_members()).unwrap(),
985 r#""manage_members""#
986 );
987 assert_eq!(
988 serde_json::to_string(&Capability::rotate_keys()).unwrap(),
989 r#""rotate_keys""#
990 );
991 }
992
993 #[test]
994 fn capability_deserializes_from_snake_case() {
995 assert_eq!(
996 serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
997 Capability::sign_commit()
998 );
999 assert_eq!(
1000 serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1001 Capability::sign_release()
1002 );
1003 assert_eq!(
1004 serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1005 Capability::manage_members()
1006 );
1007 assert_eq!(
1008 serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1009 Capability::rotate_keys()
1010 );
1011 }
1012
1013 #[test]
1014 fn capability_custom_serializes_as_string() {
1015 let cap = Capability::parse("acme:deploy").unwrap();
1016 assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1017 }
1018
1019 #[test]
1020 fn capability_custom_deserializes_unknown_strings() {
1021 let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1023 assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1024 }
1025
1026 #[test]
1031 fn capability_parse_accepts_valid_strings() {
1032 assert!(Capability::parse("deploy").is_ok());
1033 assert!(Capability::parse("acme:deploy").is_ok());
1034 assert!(Capability::parse("my-custom-cap").is_ok());
1035 assert!(Capability::parse("org:team:action").is_ok());
1036 assert!(Capability::parse("with_underscore").is_ok()); }
1038
1039 #[test]
1040 fn capability_parse_rejects_invalid_strings() {
1041 assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
1043
1044 assert!(matches!(
1046 Capability::parse(&"a".repeat(65)),
1047 Err(CapabilityError::TooLong(65))
1048 ));
1049
1050 assert!(matches!(
1052 Capability::parse("has spaces"),
1053 Err(CapabilityError::InvalidChars(_))
1054 ));
1055 assert!(matches!(
1056 Capability::parse("has.dot"),
1057 Err(CapabilityError::InvalidChars(_))
1058 ));
1059 }
1060
1061 #[test]
1062 fn capability_parse_rejects_reserved_namespace() {
1063 assert!(matches!(
1064 Capability::parse("auths:custom"),
1065 Err(CapabilityError::ReservedNamespace)
1066 ));
1067 assert!(matches!(
1068 Capability::parse("auths:sign_commit"),
1069 Err(CapabilityError::ReservedNamespace)
1070 ));
1071 }
1072
1073 #[test]
1074 fn capability_parse_normalizes_to_lowercase() {
1075 let cap = Capability::parse("DEPLOY").unwrap();
1076 assert_eq!(cap.as_str(), "deploy");
1077
1078 let cap = Capability::parse("ACME:Deploy").unwrap();
1079 assert_eq!(cap.as_str(), "acme:deploy");
1080 }
1081
1082 #[test]
1083 fn capability_parse_trims_whitespace() {
1084 let cap = Capability::parse(" deploy ").unwrap();
1085 assert_eq!(cap.as_str(), "deploy");
1086 }
1087
1088 #[test]
1093 fn capability_is_hashable() {
1094 use std::collections::HashSet;
1095 let mut set = HashSet::new();
1096 set.insert(Capability::sign_commit());
1097 set.insert(Capability::sign_release());
1098 set.insert(Capability::parse("test").unwrap());
1099 assert_eq!(set.len(), 3);
1100 assert!(set.contains(&Capability::sign_commit()));
1101 }
1102
1103 #[test]
1104 fn capability_equality_with_different_construction_paths() {
1105 let from_constructor = Capability::sign_commit();
1107 let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
1108 assert_eq!(from_constructor, from_deser);
1109
1110 let from_parse = Capability::parse("acme:deploy").unwrap();
1112 let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
1113 assert_eq!(from_parse, from_deser);
1114 }
1115
1116 #[test]
1121 fn capability_display_matches_canonical_form() {
1122 assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
1123 assert_eq!(Capability::sign_release().to_string(), "sign_release");
1124 assert_eq!(Capability::manage_members().to_string(), "manage_members");
1125 assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
1126 assert_eq!(
1127 Capability::parse("acme:deploy").unwrap().to_string(),
1128 "acme:deploy"
1129 );
1130 }
1131
1132 #[test]
1133 fn capability_as_str_returns_canonical_form() {
1134 assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
1135 assert_eq!(Capability::sign_release().as_str(), "sign_release");
1136 assert_eq!(Capability::manage_members().as_str(), "manage_members");
1137 assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
1138 assert_eq!(
1139 Capability::parse("acme:deploy").unwrap().as_str(),
1140 "acme:deploy"
1141 );
1142 }
1143
1144 #[test]
1145 fn capability_is_well_known() {
1146 assert!(Capability::sign_commit().is_well_known());
1147 assert!(Capability::sign_release().is_well_known());
1148 assert!(Capability::manage_members().is_well_known());
1149 assert!(Capability::rotate_keys().is_well_known());
1150 assert!(!Capability::parse("custom").unwrap().is_well_known());
1151 }
1152
1153 #[test]
1154 fn capability_namespace() {
1155 assert_eq!(
1156 Capability::parse("acme:deploy").unwrap().namespace(),
1157 Some("acme")
1158 );
1159 assert_eq!(
1160 Capability::parse("org:team:action").unwrap().namespace(),
1161 Some("org")
1162 );
1163 assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
1164 }
1165
1166 #[test]
1171 fn capability_vec_serializes_as_array() {
1172 let caps = vec![Capability::sign_commit(), Capability::sign_release()];
1173 let json = serde_json::to_string(&caps).unwrap();
1174 assert_eq!(json, r#"["sign_commit","sign_release"]"#);
1175 }
1176
1177 #[test]
1178 fn capability_vec_deserializes_from_array() {
1179 let json = r#"["sign_commit","manage_members","custom-cap"]"#;
1180 let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
1181 assert_eq!(caps.len(), 3);
1182 assert_eq!(caps[0], Capability::sign_commit());
1183 assert_eq!(caps[1], Capability::manage_members());
1184 assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
1185 }
1186
1187 #[test]
1192 fn capability_serde_roundtrip_well_known() {
1193 let caps = vec![
1194 Capability::sign_commit(),
1195 Capability::sign_release(),
1196 Capability::manage_members(),
1197 Capability::rotate_keys(),
1198 ];
1199 for cap in caps {
1200 let json = serde_json::to_string(&cap).unwrap();
1201 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1202 assert_eq!(cap, roundtrip);
1203 }
1204 }
1205
1206 #[test]
1207 fn capability_serde_roundtrip_custom() {
1208 let caps = vec![
1209 Capability::parse("deploy").unwrap(),
1210 Capability::parse("acme:deploy").unwrap(),
1211 Capability::parse("org:team:action").unwrap(),
1212 ];
1213 for cap in caps {
1214 let json = serde_json::to_string(&cap).unwrap();
1215 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1216 assert_eq!(cap, roundtrip);
1217 }
1218 }
1219
1220 #[test]
1223 fn attestation_old_json_without_org_fields_deserializes() {
1224 let old_json = r#"{
1226 "version": 1,
1227 "rid": "test-rid",
1228 "issuer": "did:key:issuer",
1229 "subject": "did:key:subject",
1230 "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
1231 "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1232 "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1233 "revoked_at": null,
1234 "timestamp": null
1235 }"#;
1236
1237 let att: Attestation = serde_json::from_str(old_json).unwrap();
1238
1239 assert_eq!(att.role, None);
1241 assert!(att.capabilities.is_empty());
1242 assert_eq!(att.delegated_by, None);
1243 }
1244
1245 #[test]
1246 fn attestation_with_org_fields_serializes_correctly() {
1247 use crate::types::DeviceDID;
1248
1249 let att = Attestation {
1250 version: 1,
1251 rid: ResourceId::new("test-rid"),
1252 issuer: IdentityDID::new("did:key:issuer"),
1253 subject: DeviceDID::new("did:key:subject".to_string()),
1254 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1255 identity_signature: Ed25519Signature::empty(),
1256 device_signature: Ed25519Signature::empty(),
1257 revoked_at: None,
1258 expires_at: None,
1259 timestamp: None,
1260 note: None,
1261 payload: None,
1262 role: Some(Role::Admin),
1263 capabilities: vec![Capability::sign_commit(), Capability::manage_members()],
1264 delegated_by: Some(IdentityDID::new("did:key:delegator")),
1265 signer_type: None,
1266 };
1267
1268 let json = serde_json::to_string(&att).unwrap();
1269 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1270
1271 assert_eq!(parsed["role"], "admin");
1272 assert_eq!(parsed["capabilities"][0], "sign_commit");
1273 assert_eq!(parsed["capabilities"][1], "manage_members");
1274 assert_eq!(parsed["delegated_by"], "did:key:delegator");
1275 }
1276
1277 #[test]
1278 fn attestation_without_org_fields_omits_them_in_json() {
1279 use crate::types::DeviceDID;
1280
1281 let att = Attestation {
1282 version: 1,
1283 rid: ResourceId::new("test-rid"),
1284 issuer: IdentityDID::new("did:key:issuer"),
1285 subject: DeviceDID::new("did:key:subject".to_string()),
1286 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1287 identity_signature: Ed25519Signature::empty(),
1288 device_signature: Ed25519Signature::empty(),
1289 revoked_at: None,
1290 expires_at: None,
1291 timestamp: None,
1292 note: None,
1293 payload: None,
1294 role: None,
1295 capabilities: vec![],
1296 delegated_by: None,
1297 signer_type: None,
1298 };
1299
1300 let json = serde_json::to_string(&att).unwrap();
1301 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1302
1303 assert!(parsed.get("role").is_none());
1305 assert!(parsed.get("capabilities").is_none());
1306 assert!(parsed.get("delegated_by").is_none());
1307 }
1308
1309 #[test]
1310 fn attestation_with_org_fields_roundtrips() {
1311 use crate::types::DeviceDID;
1312
1313 let original = Attestation {
1314 version: 1,
1315 rid: ResourceId::new("test-rid"),
1316 issuer: IdentityDID::new("did:key:issuer"),
1317 subject: DeviceDID::new("did:key:subject".to_string()),
1318 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1319 identity_signature: Ed25519Signature::empty(),
1320 device_signature: Ed25519Signature::empty(),
1321 revoked_at: None,
1322 expires_at: None,
1323 timestamp: None,
1324 note: None,
1325 payload: None,
1326 role: Some(Role::Member),
1327 capabilities: vec![Capability::sign_commit(), Capability::sign_release()],
1328 delegated_by: Some(IdentityDID::new("did:key:admin")),
1329 signer_type: None,
1330 };
1331
1332 let json = serde_json::to_string(&original).unwrap();
1333 let deserialized: Attestation = serde_json::from_str(&json).unwrap();
1334
1335 assert_eq!(original.role, deserialized.role);
1336 assert_eq!(original.capabilities, deserialized.capabilities);
1337 assert_eq!(original.delegated_by, deserialized.delegated_by);
1338 }
1339
1340 #[test]
1343 fn threshold_policy_new_creates_valid_policy() {
1344 let policy = ThresholdPolicy::new(
1345 2,
1346 vec![
1347 "did:key:alice".to_string(),
1348 "did:key:bob".to_string(),
1349 "did:key:carol".to_string(),
1350 ],
1351 "test-policy".to_string(),
1352 );
1353
1354 assert_eq!(policy.threshold, 2);
1355 assert_eq!(policy.signers.len(), 3);
1356 assert_eq!(policy.policy_id, "test-policy");
1357 assert!(policy.scope.is_none());
1358 assert!(policy.ceremony_endpoint.is_none());
1359 }
1360
1361 #[test]
1362 fn threshold_policy_is_valid_checks_constraints() {
1363 let valid = ThresholdPolicy::new(
1365 2,
1366 vec!["a".to_string(), "b".to_string(), "c".to_string()],
1367 "policy".to_string(),
1368 );
1369 assert!(valid.is_valid());
1370
1371 let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
1373 assert!(!zero_threshold.is_valid());
1374
1375 let too_high = ThresholdPolicy::new(
1377 3,
1378 vec!["a".to_string(), "b".to_string()],
1379 "policy".to_string(),
1380 );
1381 assert!(!too_high.is_valid());
1382
1383 let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
1385 assert!(!no_signers.is_valid());
1386
1387 let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
1389 assert!(!no_id.is_valid());
1390 }
1391
1392 #[test]
1393 fn threshold_policy_m_of_n_returns_correct_values() {
1394 let policy = ThresholdPolicy::new(
1395 2,
1396 vec!["a".to_string(), "b".to_string(), "c".to_string()],
1397 "policy".to_string(),
1398 );
1399 let (m, n) = policy.m_of_n();
1400 assert_eq!(m, 2);
1401 assert_eq!(n, 3);
1402 }
1403
1404 #[test]
1405 fn threshold_policy_serializes_correctly() {
1406 let mut policy = ThresholdPolicy::new(
1407 2,
1408 vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
1409 "release-policy".to_string(),
1410 );
1411 policy.scope = Some(Capability::sign_release());
1412 policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
1413
1414 let json = serde_json::to_string(&policy).unwrap();
1415 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1416
1417 assert_eq!(parsed["threshold"], 2);
1418 assert_eq!(parsed["signers"][0], "did:key:alice");
1419 assert_eq!(parsed["policy_id"], "release-policy");
1420 assert_eq!(parsed["scope"], "sign_release");
1421 assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
1422 }
1423
1424 #[test]
1425 fn threshold_policy_without_optional_fields_omits_them() {
1426 let policy =
1427 ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
1428
1429 let json = serde_json::to_string(&policy).unwrap();
1430 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1431
1432 assert!(parsed.get("scope").is_none());
1433 assert!(parsed.get("ceremony_endpoint").is_none());
1434 }
1435
1436 #[test]
1437 fn threshold_policy_roundtrips() {
1438 let mut original = ThresholdPolicy::new(
1439 3,
1440 vec![
1441 "a".to_string(),
1442 "b".to_string(),
1443 "c".to_string(),
1444 "d".to_string(),
1445 ],
1446 "important-policy".to_string(),
1447 );
1448 original.scope = Some(Capability::rotate_keys());
1449
1450 let json = serde_json::to_string(&original).unwrap();
1451 let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
1452
1453 assert_eq!(original, deserialized);
1454 }
1455
1456 #[test]
1459 fn identity_bundle_serializes_correctly() {
1460 let bundle = IdentityBundle {
1461 identity_did: "did:keri:test123".to_string(),
1462 public_key_hex: "aabbccdd".to_string(),
1463 attestation_chain: vec![],
1464 bundle_timestamp: Utc::now(),
1465 max_valid_for_secs: 86400,
1466 };
1467
1468 let json = serde_json::to_string(&bundle).unwrap();
1469 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1470
1471 assert_eq!(parsed["identity_did"], "did:keri:test123");
1472 assert_eq!(parsed["public_key_hex"], "aabbccdd");
1473 assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
1474 }
1475
1476 #[test]
1477 fn identity_bundle_deserializes_correctly() {
1478 let json = r#"{
1479 "identity_did": "did:keri:abc123",
1480 "public_key_hex": "112233",
1481 "attestation_chain": [],
1482 "bundle_timestamp": "2099-01-01T00:00:00Z",
1483 "max_valid_for_secs": 86400
1484 }"#;
1485
1486 let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
1487
1488 assert_eq!(bundle.identity_did, "did:keri:abc123");
1489 assert_eq!(bundle.public_key_hex, "112233");
1490 assert!(bundle.attestation_chain.is_empty());
1491 }
1492
1493 #[test]
1494 fn identity_bundle_roundtrips() {
1495 use crate::types::DeviceDID;
1496
1497 let attestation = Attestation {
1498 version: 1,
1499 rid: ResourceId::new("test-rid"),
1500 issuer: IdentityDID::new("did:key:issuer"),
1501 subject: DeviceDID::new("did:key:subject".to_string()),
1502 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1503 identity_signature: Ed25519Signature::empty(),
1504 device_signature: Ed25519Signature::empty(),
1505 revoked_at: None,
1506 expires_at: None,
1507 timestamp: None,
1508 note: None,
1509 payload: None,
1510 role: None,
1511 capabilities: vec![],
1512 delegated_by: None,
1513 signer_type: None,
1514 };
1515
1516 let original = IdentityBundle {
1517 identity_did: "did:keri:example".to_string(),
1518 public_key_hex: "deadbeef".to_string(),
1519 attestation_chain: vec![attestation],
1520 bundle_timestamp: Utc::now(),
1521 max_valid_for_secs: 86400,
1522 };
1523
1524 let json = serde_json::to_string(&original).unwrap();
1525 let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
1526
1527 assert_eq!(original.identity_did, deserialized.identity_did);
1528 assert_eq!(original.public_key_hex, deserialized.public_key_hex);
1529 assert_eq!(
1530 original.attestation_chain.len(),
1531 deserialized.attestation_chain.len()
1532 );
1533 }
1534}