1use crate::error::AttestationError;
4use crate::types::{CanonicalDid, 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
277pub const ATTESTATION_VERSION: u32 = 2;
287
288#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct TypedSignature([u8; 64]);
298
299pub type Ed25519Signature = TypedSignature;
301
302impl TypedSignature {
303 pub fn from_bytes(bytes: [u8; 64]) -> Self {
305 Self(bytes)
306 }
307
308 pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
310 let arr: [u8; 64] = slice
311 .try_into()
312 .map_err(|_| SignatureLengthError(slice.len()))?;
313 Ok(Self(arr))
314 }
315
316 pub fn empty() -> Self {
318 Self([0u8; 64])
319 }
320
321 pub fn is_empty(&self) -> bool {
323 self.0 == [0u8; 64]
324 }
325
326 pub fn as_bytes(&self) -> &[u8; 64] {
328 &self.0
329 }
330}
331
332impl Default for TypedSignature {
333 fn default() -> Self {
334 Self::empty()
335 }
336}
337
338impl std::fmt::Display for TypedSignature {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 write!(f, "{}", hex::encode(self.0))
341 }
342}
343
344impl AsRef<[u8]> for TypedSignature {
345 fn as_ref(&self) -> &[u8] {
346 &self.0
347 }
348}
349
350#[cfg(feature = "schema")]
351impl schemars::JsonSchema for TypedSignature {
352 fn schema_name() -> String {
353 "TypedSignature".to_owned()
354 }
355
356 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
357 schemars::schema::SchemaObject {
358 instance_type: Some(schemars::schema::InstanceType::String.into()),
359 format: Some("hex".to_owned()),
360 metadata: Some(Box::new(schemars::schema::Metadata {
361 description: Some(
362 "Curve-agnostic 64-byte signature (Ed25519 or P-256 r||s, hex-encoded). \
363 Curve is determined by the companion DevicePublicKey."
364 .to_owned(),
365 ),
366 ..Default::default()
367 })),
368 ..Default::default()
369 }
370 .into()
371 }
372}
373
374impl serde::Serialize for TypedSignature {
375 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
376 serializer.serialize_str(&hex::encode(self.0))
377 }
378}
379
380impl<'de> serde::Deserialize<'de> for TypedSignature {
381 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382 let s = String::deserialize(deserializer)?;
383 if s.is_empty() {
384 return Ok(Self::empty());
385 }
386 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
387 Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
393#[error("expected 64 bytes, got {0}")]
394pub struct SignatureLengthError(pub usize);
395
396#[derive(Debug, Clone)]
406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
407pub struct DevicePublicKey {
408 #[cfg_attr(feature = "schema", schemars(skip))]
409 curve: auths_crypto::CurveType,
410 #[cfg_attr(feature = "schema", schemars(with = "String"))]
411 bytes: Vec<u8>,
412}
413
414impl DevicePublicKey {
415 fn canonical_sec1(&self) -> std::borrow::Cow<'_, [u8]> {
421 if self.curve == auths_crypto::CurveType::P256
422 && self.bytes.len() == 65
423 && self.bytes[0] == 0x04
424 {
425 let mut compressed = vec![0u8; 33];
426 compressed[0] = if self.bytes[64] & 1 == 0 { 0x02 } else { 0x03 };
427 compressed[1..].copy_from_slice(&self.bytes[1..33]);
428 std::borrow::Cow::Owned(compressed)
429 } else {
430 std::borrow::Cow::Borrowed(&self.bytes)
431 }
432 }
433}
434
435impl PartialEq for DevicePublicKey {
436 fn eq(&self, other: &Self) -> bool {
437 self.curve == other.curve && self.canonical_sec1() == other.canonical_sec1()
438 }
439}
440
441impl Eq for DevicePublicKey {}
442
443impl std::hash::Hash for DevicePublicKey {
444 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
445 self.curve.hash(state);
446 self.canonical_sec1().hash(state);
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
452pub enum InvalidKeyError {
453 #[error("invalid key length for {curve}: expected {expected}, got {actual}")]
455 InvalidLength {
456 curve: auths_crypto::CurveType,
458 expected: &'static str,
460 actual: usize,
462 },
463}
464
465impl DevicePublicKey {
466 pub fn try_new(curve: auths_crypto::CurveType, bytes: &[u8]) -> Result<Self, InvalidKeyError> {
477 let valid = match curve {
478 auths_crypto::CurveType::Ed25519 => bytes.len() == 32,
479 auths_crypto::CurveType::P256 => bytes.len() == 33 || bytes.len() == 65,
480 };
481 if !valid {
482 return Err(InvalidKeyError::InvalidLength {
483 curve,
484 expected: match curve {
485 auths_crypto::CurveType::Ed25519 => "32",
486 auths_crypto::CurveType::P256 => "33 or 65",
487 },
488 actual: bytes.len(),
489 });
490 }
491 Ok(Self {
492 curve,
493 bytes: bytes.to_vec(),
494 })
495 }
496
497 pub fn ed25519(bytes: &[u8; 32]) -> Self {
499 Self {
500 curve: auths_crypto::CurveType::Ed25519,
501 bytes: bytes.to_vec(),
502 }
503 }
504
505 pub fn p256(bytes: &[u8]) -> Result<Self, InvalidKeyError> {
509 Self::try_new(auths_crypto::CurveType::P256, bytes)
510 }
511
512 pub fn curve(&self) -> auths_crypto::CurveType {
514 self.curve
515 }
516
517 pub fn as_bytes(&self) -> &[u8] {
519 &self.bytes
520 }
521
522 pub fn is_zero(&self) -> bool {
524 self.bytes.iter().all(|&b| b == 0)
525 }
526
527 pub fn len(&self) -> usize {
529 self.bytes.len()
530 }
531
532 pub fn is_empty(&self) -> bool {
534 self.bytes.is_empty()
535 }
536
537 pub async fn verify(
556 &self,
557 message: &[u8],
558 signature: &[u8],
559 provider: &dyn auths_crypto::CryptoProvider,
560 ) -> Result<(), SignatureVerifyError> {
561 let result = match self.curve {
562 auths_crypto::CurveType::Ed25519 => {
563 provider
564 .verify_ed25519(&self.bytes, message, signature)
565 .await
566 }
567 auths_crypto::CurveType::P256 => {
568 provider.verify_p256(&self.bytes, message, signature).await
569 }
570 };
571 result.map_err(|e| SignatureVerifyError::VerificationFailed(e.to_string()))
572 }
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
578pub enum PublicKeyDecodeError {
579 #[error("invalid hex: {0}")]
581 InvalidHex(String),
582
583 #[error("invalid public key length {len} — expected 32 (Ed25519) or 33/65 (P-256)")]
586 InvalidLength {
587 len: usize,
589 },
590
591 #[error("DevicePublicKey validation failed: {0}")]
594 Validation(String),
595}
596
597pub fn decode_public_key_hex(
614 hex_str: &str,
615 curve: auths_crypto::CurveType,
616) -> Result<DevicePublicKey, PublicKeyDecodeError> {
617 let bytes =
618 hex::decode(hex_str.trim()).map_err(|e| PublicKeyDecodeError::InvalidHex(e.to_string()))?;
619 decode_public_key_bytes(&bytes, curve)
620}
621
622pub fn decode_public_key_bytes(
634 bytes: &[u8],
635 curve: auths_crypto::CurveType,
636) -> Result<DevicePublicKey, PublicKeyDecodeError> {
637 DevicePublicKey::try_new(curve, bytes)
638 .map_err(|e| PublicKeyDecodeError::Validation(e.to_string()))
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
643pub enum SignatureVerifyError {
644 #[error("signature verification failed: {0}")]
646 VerificationFailed(String),
647
648 #[error("{0}")]
651 UnsupportedOnTarget(String),
652}
653
654impl From<Ed25519PublicKey> for DevicePublicKey {
655 fn from(pk: Ed25519PublicKey) -> Self {
656 Self {
657 curve: auths_crypto::CurveType::Ed25519,
658 bytes: pk.as_bytes().to_vec(),
659 }
660 }
661}
662
663impl Serialize for DevicePublicKey {
664 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
665 use serde::ser::SerializeStruct;
666 let mut st = s.serialize_struct("DevicePublicKey", 2)?;
667 st.serialize_field("curve", &self.curve.to_string())?;
668 st.serialize_field("key", &hex::encode(&self.bytes))?;
669 st.end()
670 }
671}
672
673impl<'de> Deserialize<'de> for DevicePublicKey {
674 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
675 let value = serde_json::Value::deserialize(d)?;
679
680 if let Some(s) = value.as_str() {
681 if s.is_empty() {
683 return Ok(Self::default());
684 }
685 let bytes = hex::decode(s)
686 .map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
687 let curve = match bytes.len() {
688 32 => auths_crypto::CurveType::Ed25519,
689 33 | 65 => auths_crypto::CurveType::P256,
690 n => {
691 return Err(serde::de::Error::custom(format!(
692 "invalid device public key length: {n}"
693 )));
694 }
695 };
696 return Self::try_new(curve, &bytes)
697 .map_err(|e| serde::de::Error::custom(e.to_string()));
698 }
699
700 let curve_str = value["curve"]
702 .as_str()
703 .ok_or_else(|| serde::de::Error::custom("missing 'curve' field"))?;
704 let key_hex = value["key"]
705 .as_str()
706 .ok_or_else(|| serde::de::Error::custom("missing 'key' field"))?;
707
708 let curve = match curve_str {
709 "ed25519" => auths_crypto::CurveType::Ed25519,
710 "p256" => auths_crypto::CurveType::P256,
711 other => {
712 return Err(serde::de::Error::custom(format!("unknown curve: {other}")));
713 }
714 };
715 if key_hex.is_empty() {
716 return Err(serde::de::Error::custom("empty key"));
717 }
718 let bytes = hex::decode(key_hex)
719 .map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
720 Self::try_new(curve, &bytes).map_err(|e| serde::de::Error::custom(e.to_string()))
721 }
722}
723
724impl Default for DevicePublicKey {
725 fn default() -> Self {
726 Self {
727 curve: auths_crypto::CurveType::Ed25519,
728 bytes: vec![0u8; 32],
729 }
730 }
731}
732
733impl fmt::Display for DevicePublicKey {
734 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
735 write!(f, "{}:{}", self.curve, hex::encode(&self.bytes))
736 }
737}
738
739#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
756#[serde(rename_all = "snake_case")]
757pub enum SignatureAlgorithm {
758 #[default]
760 Ed25519,
761 EcdsaP256,
763}
764
765impl fmt::Display for SignatureAlgorithm {
766 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
767 match self {
768 Self::Ed25519 => f.write_str("ed25519"),
769 Self::EcdsaP256 => f.write_str("ecdsa_p256"),
770 }
771 }
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
788pub struct EcdsaP256PublicKey(Vec<u8>);
789
790impl EcdsaP256PublicKey {
791 pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
796 const P256_OID: &[u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
799 if der.len() < 26 {
800 return Err(EcdsaP256Error::InvalidKey(
801 "DER too short for P-256 PKIX key".into(),
802 ));
803 }
804 if !der.windows(P256_OID.len()).any(|w| w == P256_OID) {
805 return Err(EcdsaP256Error::InvalidKey(
806 "missing P-256 OID in PKIX key".into(),
807 ));
808 }
809 Ok(Self(der.to_vec()))
810 }
811
812 pub fn as_der(&self) -> &[u8] {
814 &self.0
815 }
816
817 pub fn as_sec1_uncompressed(&self) -> Option<&[u8]> {
832 let start = self.0.len().checked_sub(65)?;
833 let point = &self.0[start..];
834 (point[0] == 0x04).then_some(point)
835 }
836}
837
838impl Serialize for EcdsaP256PublicKey {
839 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
840 use base64::Engine;
841 s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
842 }
843}
844
845impl<'de> Deserialize<'de> for EcdsaP256PublicKey {
846 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
847 use base64::Engine;
848 let s = String::deserialize(d)?;
849 let bytes = base64::engine::general_purpose::STANDARD
850 .decode(&s)
851 .map_err(|e| serde::de::Error::custom(format!("invalid base64: {e}")))?;
852 Self::from_der(&bytes).map_err(serde::de::Error::custom)
853 }
854}
855
856#[derive(Debug, Clone, PartialEq, Eq)]
865pub struct EcdsaP256Signature(Vec<u8>);
866
867impl EcdsaP256Signature {
868 pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
870 if der.is_empty() || der[0] != 0x30 {
872 return Err(EcdsaP256Error::InvalidSignature(
873 "not an ASN.1 SEQUENCE".into(),
874 ));
875 }
876 Ok(Self(der.to_vec()))
877 }
878
879 pub fn as_der(&self) -> &[u8] {
881 &self.0
882 }
883}
884
885impl Serialize for EcdsaP256Signature {
886 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
887 use base64::Engine;
888 s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
889 }
890}
891
892impl<'de> Deserialize<'de> for EcdsaP256Signature {
893 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
894 use base64::Engine;
895 let s = String::deserialize(d)?;
896 let bytes = base64::engine::general_purpose::STANDARD
897 .decode(&s)
898 .map_err(|e| serde::de::Error::custom(format!("invalid base64: {e}")))?;
899 Self::from_der(&bytes).map_err(serde::de::Error::custom)
900 }
901}
902
903#[derive(Debug, Clone, thiserror::Error)]
905pub enum EcdsaP256Error {
906 #[error("invalid ECDSA P-256 key: {0}")]
908 InvalidKey(String),
909 #[error("invalid ECDSA P-256 signature: {0}")]
911 InvalidSignature(String),
912}
913
914#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
920pub enum CapabilityError {
921 #[error("capability is empty")]
923 Empty,
924 #[error("capability exceeds 64 chars: {0}")]
926 TooLong(usize),
927 #[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
929 InvalidChars(String),
930 #[error(
932 "reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
933 )]
934 ReservedNamespace,
935 #[error("the '{0}' prefix is reserved for infrastructure capabilities")]
937 ReservedInfraNamespace(String),
938}
939
940#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
968#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
969#[serde(try_from = "String", into = "String")]
970pub struct Capability(String);
971
972impl Capability {
973 pub const MAX_LEN: usize = 64;
975
976 const RESERVED_PREFIX: &'static str = "auths:";
978
979 const RESERVED_INFRA_PREFIXES: &'static [&'static str] =
981 &["compute:", "network:", "storage:", "runtime:", "env:"];
982
983 #[inline]
991 pub fn sign_commit() -> Self {
992 Self(SIGN_COMMIT.to_string())
993 }
994
995 #[inline]
999 pub fn sign_release() -> Self {
1000 Self(SIGN_RELEASE.to_string())
1001 }
1002
1003 #[inline]
1007 pub fn manage_members() -> Self {
1008 Self(MANAGE_MEMBERS.to_string())
1009 }
1010
1011 #[inline]
1015 pub fn rotate_keys() -> Self {
1016 Self(ROTATE_KEYS.to_string())
1017 }
1018
1019 pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
1051 let canonical = raw.trim().to_lowercase();
1052
1053 if canonical.is_empty() {
1054 return Err(CapabilityError::Empty);
1055 }
1056 if canonical.len() > Self::MAX_LEN {
1057 return Err(CapabilityError::TooLong(canonical.len()));
1058 }
1059 if !canonical
1060 .chars()
1061 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
1062 {
1063 return Err(CapabilityError::InvalidChars(canonical));
1064 }
1065 if canonical.starts_with(Self::RESERVED_PREFIX) {
1066 return Err(CapabilityError::ReservedNamespace);
1067 }
1068 for prefix in Self::RESERVED_INFRA_PREFIXES {
1069 if canonical.starts_with(prefix) {
1070 return Err(CapabilityError::ReservedInfraNamespace(prefix.to_string()));
1071 }
1072 }
1073
1074 Ok(Self(canonical))
1075 }
1076
1077 #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
1085 pub fn custom(s: impl Into<String>) -> Option<Self> {
1086 Self::parse(&s.into()).ok()
1087 }
1088
1089 #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
1095 pub fn validate_custom(s: &str) -> bool {
1096 Self::parse(s).is_ok()
1097 }
1098
1099 #[inline]
1108 pub fn as_str(&self) -> &str {
1109 &self.0
1110 }
1111
1112 pub fn is_well_known(&self) -> bool {
1114 matches!(
1115 self.0.as_str(),
1116 SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
1117 )
1118 }
1119
1120 pub fn namespace(&self) -> Option<&str> {
1122 self.0.split(':').next().filter(|_| self.0.contains(':'))
1123 }
1124}
1125
1126impl fmt::Display for Capability {
1127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1128 f.write_str(&self.0)
1129 }
1130}
1131
1132impl TryFrom<String> for Capability {
1133 type Error = CapabilityError;
1134
1135 fn try_from(s: String) -> Result<Self, Self::Error> {
1136 let canonical = s.trim().to_lowercase();
1137
1138 if canonical.is_empty() {
1139 return Err(CapabilityError::Empty);
1140 }
1141 if canonical.len() > Self::MAX_LEN {
1142 return Err(CapabilityError::TooLong(canonical.len()));
1143 }
1144 if !canonical
1145 .chars()
1146 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
1147 {
1148 return Err(CapabilityError::InvalidChars(canonical));
1149 }
1150
1151 Ok(Self(canonical))
1154 }
1155}
1156
1157impl std::str::FromStr for Capability {
1158 type Err = CapabilityError;
1159
1160 fn from_str(s: &str) -> Result<Self, Self::Err> {
1179 let normalized = s.trim().to_lowercase().replace('-', "_");
1180 match normalized.as_str() {
1181 "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
1182 "sign_release" | "signrelease" => Ok(Capability::sign_release()),
1183 "manage_members" | "managemembers" => Ok(Capability::manage_members()),
1184 "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
1185 _ => Capability::parse(&normalized),
1186 }
1187 }
1188}
1189
1190impl From<Capability> for String {
1191 fn from(cap: Capability) -> Self {
1192 cap.0
1193 }
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1202pub struct IdentityBundle {
1203 pub identity_did: IdentityDID,
1205 pub public_key_hex: PublicKeyHex,
1207 #[serde(default)]
1211 #[cfg_attr(feature = "schema", schemars(with = "String"))]
1212 pub curve: auths_crypto::CurveType,
1213 pub attestation_chain: Vec<Attestation>,
1215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1219 pub kel: Vec<serde_json::Value>,
1220 pub bundle_timestamp: DateTime<Utc>,
1222 pub max_valid_for_secs: u64,
1224}
1225
1226impl IdentityBundle {
1227 pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
1237 let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
1238 if age > self.max_valid_for_secs {
1239 return Err(AttestationError::BundleExpired {
1240 age_secs: age,
1241 max_secs: self.max_valid_for_secs,
1242 });
1243 }
1244 Ok(())
1245 }
1246}
1247
1248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1250#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1251pub struct Attestation {
1252 pub version: u32,
1254 pub rid: ResourceId,
1256 pub issuer: CanonicalDid,
1258 pub subject: CanonicalDid,
1260 pub device_public_key: DevicePublicKey,
1262 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
1264 pub identity_signature: Ed25519Signature,
1265 pub device_signature: Ed25519Signature,
1267 #[serde(default, skip_serializing_if = "Option::is_none")]
1269 pub revoked_at: Option<DateTime<Utc>>,
1270 #[serde(skip_serializing_if = "Option::is_none")]
1272 pub expires_at: Option<DateTime<Utc>>,
1273 pub timestamp: Option<DateTime<Utc>>,
1275 #[serde(skip_serializing_if = "Option::is_none")]
1277 pub note: Option<String>,
1278 #[serde(skip_serializing_if = "Option::is_none")]
1280 pub payload: Option<Value>,
1281
1282 #[serde(skip_serializing_if = "Option::is_none")]
1284 pub commit_sha: Option<String>,
1285
1286 #[serde(skip_serializing_if = "Option::is_none")]
1288 pub commit_message: Option<String>,
1289
1290 #[serde(skip_serializing_if = "Option::is_none")]
1292 pub author: Option<String>,
1293
1294 #[serde(skip_serializing_if = "Option::is_none")]
1296 pub oidc_binding: Option<OidcBinding>,
1297
1298 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub delegated_by: Option<CanonicalDid>,
1301
1302 #[serde(default, skip_serializing_if = "Option::is_none")]
1305 pub signer_type: Option<SignerType>,
1306
1307 #[serde(default, skip_serializing_if = "Option::is_none")]
1310 pub environment_claim: Option<Value>,
1311}
1312
1313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1319#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1320pub struct OidcBinding {
1321 pub issuer: String,
1323 pub subject: String,
1325 pub audience: String,
1327 pub token_exp: i64,
1329 #[serde(default, skip_serializing_if = "Option::is_none")]
1331 pub platform: Option<String>,
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1334 pub jti: Option<String>,
1335 #[serde(default, skip_serializing_if = "Option::is_none")]
1337 pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
1338}
1339
1340#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1345#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1346#[non_exhaustive]
1347pub enum SignerType {
1348 Human,
1350 Agent,
1352 Workload,
1354}
1355
1356#[derive(Debug, Clone, Serialize)]
1366pub struct VerifiedAttestation(Attestation);
1367
1368impl VerifiedAttestation {
1369 pub fn inner(&self) -> &Attestation {
1371 &self.0
1372 }
1373
1374 pub fn into_inner(self) -> Attestation {
1376 self.0
1377 }
1378
1379 #[doc(hidden)]
1385 pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
1386 Self(attestation)
1387 }
1388
1389 pub(crate) fn from_verified(attestation: Attestation) -> Self {
1390 Self(attestation)
1391 }
1392}
1393
1394impl std::ops::Deref for VerifiedAttestation {
1395 type Target = Attestation;
1396
1397 fn deref(&self) -> &Attestation {
1398 &self.0
1399 }
1400}
1401
1402#[derive(Serialize, Debug)]
1404pub struct CanonicalAttestationData<'a> {
1405 pub version: u32,
1407 pub rid: &'a str,
1409 pub issuer: &'a CanonicalDid,
1411 pub subject: &'a CanonicalDid,
1413 #[serde(with = "hex::serde")]
1415 pub device_public_key: &'a [u8],
1416 pub payload: &'a Option<Value>,
1418 pub timestamp: &'a Option<DateTime<Utc>>,
1420 pub expires_at: &'a Option<DateTime<Utc>>,
1422 pub revoked_at: &'a Option<DateTime<Utc>>,
1424 pub note: &'a Option<String>,
1426
1427 #[serde(skip_serializing_if = "Option::is_none")]
1429 pub delegated_by: Option<&'a CanonicalDid>,
1430 #[serde(skip_serializing_if = "Option::is_none")]
1432 pub signer_type: Option<&'a SignerType>,
1433 #[serde(skip_serializing_if = "Option::is_none")]
1435 pub commit_sha: Option<&'a str>,
1436}
1437
1438pub fn canonicalize_attestation_data(
1443 data: &CanonicalAttestationData,
1444) -> Result<Vec<u8>, AttestationError> {
1445 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
1446 AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
1447 })?;
1448 debug!(
1449 "Generated canonical data (standard): {}",
1450 canonical_json_string
1451 );
1452 Ok(canonical_json_string.into_bytes())
1453}
1454
1455impl Attestation {
1456 pub fn is_revoked(&self) -> bool {
1458 self.revoked_at.is_some()
1459 }
1460
1461 pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
1465 if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
1466 return Err(AttestationError::InputTooLarge(format!(
1467 "attestation JSON is {} bytes, max {}",
1468 json_bytes.len(),
1469 MAX_ATTESTATION_JSON_SIZE
1470 )));
1471 }
1472 serde_json::from_slice(json_bytes)
1473 .map_err(|e| AttestationError::SerializationError(e.to_string()))
1474 }
1475
1476 pub fn canonical_data(&self) -> CanonicalAttestationData<'_> {
1487 CanonicalAttestationData {
1488 version: self.version,
1489 rid: &self.rid,
1490 issuer: &self.issuer,
1491 subject: &self.subject,
1492 device_public_key: self.device_public_key.as_bytes(),
1493 payload: &self.payload,
1494 timestamp: &self.timestamp,
1495 expires_at: &self.expires_at,
1496 revoked_at: &self.revoked_at,
1497 note: &self.note,
1498 delegated_by: self.delegated_by.as_ref(),
1499 signer_type: self.signer_type.as_ref(),
1500 commit_sha: self.commit_sha.as_deref(),
1501 }
1502 }
1503
1504 pub fn to_debug_string(&self) -> String {
1506 format!(
1507 "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
1508 self.rid,
1509 self.issuer,
1510 self.subject, hex::encode(self.device_public_key.as_bytes()),
1512 hex::encode(self.identity_signature.as_bytes()),
1513 hex::encode(self.device_signature.as_bytes()),
1514 self.revoked_at,
1515 self.expires_at,
1516 self.note
1517 )
1518 }
1519}
1520
1521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1589pub struct ThresholdPolicy {
1590 pub threshold: u8,
1592
1593 pub signers: Vec<String>,
1595
1596 pub policy_id: PolicyId,
1598
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1601 pub scope: Option<Capability>,
1602
1603 #[serde(default, skip_serializing_if = "Option::is_none")]
1605 pub ceremony_endpoint: Option<String>,
1606}
1607
1608impl ThresholdPolicy {
1609 pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1611 Self {
1612 threshold,
1613 signers,
1614 policy_id: policy_id.into(),
1615 scope: None,
1616 ceremony_endpoint: None,
1617 }
1618 }
1619
1620 pub fn is_valid(&self) -> bool {
1622 if self.threshold < 1 {
1624 return false;
1625 }
1626 if self.threshold as usize > self.signers.len() {
1628 return false;
1629 }
1630 if self.signers.is_empty() {
1632 return false;
1633 }
1634 if self.policy_id.is_empty() {
1636 return false;
1637 }
1638 true
1639 }
1640
1641 pub fn m_of_n(&self) -> (u8, usize) {
1643 (self.threshold, self.signers.len())
1644 }
1645}
1646
1647#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1653pub enum CommitOidError {
1654 #[error("commit OID is empty")]
1656 Empty,
1657 #[error("expected 40 or 64 hex chars, got {0}")]
1659 InvalidLength(usize),
1660 #[error("invalid hex character in commit OID")]
1662 InvalidHex,
1663}
1664
1665#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1669#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1670#[repr(transparent)]
1671#[serde(try_from = "String")]
1672pub struct CommitOid(String);
1673
1674impl CommitOid {
1675 pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1685 let s = raw.trim().to_lowercase();
1686 if s.is_empty() {
1687 return Err(CommitOidError::Empty);
1688 }
1689 if s.len() != 40 && s.len() != 64 {
1690 return Err(CommitOidError::InvalidLength(s.len()));
1691 }
1692 if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1693 return Err(CommitOidError::InvalidHex);
1694 }
1695 Ok(Self(s))
1696 }
1697
1698 pub fn new_unchecked(s: impl Into<String>) -> Self {
1702 Self(s.into())
1703 }
1704
1705 pub fn as_str(&self) -> &str {
1707 &self.0
1708 }
1709
1710 pub fn into_inner(self) -> String {
1712 self.0
1713 }
1714}
1715
1716impl fmt::Display for CommitOid {
1717 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1718 f.write_str(&self.0)
1719 }
1720}
1721
1722impl AsRef<str> for CommitOid {
1723 fn as_ref(&self) -> &str {
1724 &self.0
1725 }
1726}
1727
1728impl TryFrom<String> for CommitOid {
1729 type Error = CommitOidError;
1730 fn try_from(s: String) -> Result<Self, Self::Error> {
1731 Self::parse(&s)
1732 }
1733}
1734
1735impl TryFrom<&str> for CommitOid {
1736 type Error = CommitOidError;
1737 fn try_from(s: &str) -> Result<Self, Self::Error> {
1738 Self::parse(s)
1739 }
1740}
1741
1742impl FromStr for CommitOid {
1743 type Err = CommitOidError;
1744 fn from_str(s: &str) -> Result<Self, Self::Err> {
1745 Self::parse(s)
1746 }
1747}
1748
1749impl From<CommitOid> for String {
1750 fn from(oid: CommitOid) -> Self {
1751 oid.0
1752 }
1753}
1754
1755impl<'de> Deserialize<'de> for CommitOid {
1756 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1757 let s = String::deserialize(d)?;
1758 Self::parse(&s).map_err(serde::de::Error::custom)
1759 }
1760}
1761
1762#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1768pub enum PublicKeyHexError {
1769 #[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
1771 InvalidLength(usize),
1772 #[error("invalid hex: {0}")]
1774 InvalidHex(String),
1775}
1776
1777#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1779#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1780#[repr(transparent)]
1781#[serde(try_from = "String")]
1782pub struct PublicKeyHex(String);
1783
1784impl PublicKeyHex {
1785 pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1795 let s = raw.trim().to_lowercase();
1796 let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1797 if bytes.len() != 32 && bytes.len() != 33 {
1798 return Err(PublicKeyHexError::InvalidLength(s.len()));
1799 }
1800 Ok(Self(s))
1801 }
1802
1803 pub fn new_unchecked(s: impl Into<String>) -> Self {
1807 Self(s.into())
1808 }
1809
1810 pub fn as_str(&self) -> &str {
1812 &self.0
1813 }
1814
1815 pub fn into_inner(self) -> String {
1817 self.0
1818 }
1819}
1820
1821impl fmt::Display for PublicKeyHex {
1822 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1823 f.write_str(&self.0)
1824 }
1825}
1826
1827impl AsRef<str> for PublicKeyHex {
1828 fn as_ref(&self) -> &str {
1829 &self.0
1830 }
1831}
1832
1833impl TryFrom<String> for PublicKeyHex {
1834 type Error = PublicKeyHexError;
1835 fn try_from(s: String) -> Result<Self, Self::Error> {
1836 Self::parse(&s)
1837 }
1838}
1839
1840impl TryFrom<&str> for PublicKeyHex {
1841 type Error = PublicKeyHexError;
1842 fn try_from(s: &str) -> Result<Self, Self::Error> {
1843 Self::parse(s)
1844 }
1845}
1846
1847impl FromStr for PublicKeyHex {
1848 type Err = PublicKeyHexError;
1849 fn from_str(s: &str) -> Result<Self, Self::Err> {
1850 Self::parse(s)
1851 }
1852}
1853
1854impl From<PublicKeyHex> for String {
1855 fn from(pk: PublicKeyHex) -> Self {
1856 pk.0
1857 }
1858}
1859
1860impl<'de> Deserialize<'de> for PublicKeyHex {
1861 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1862 let s = String::deserialize(d)?;
1863 Self::parse(&s).map_err(serde::de::Error::custom)
1864 }
1865}
1866
1867#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1877#[serde(transparent)]
1878pub struct PolicyId(String);
1879
1880impl PolicyId {
1881 pub fn new(s: impl Into<String>) -> Self {
1883 Self(s.into())
1884 }
1885
1886 pub fn as_str(&self) -> &str {
1888 &self.0
1889 }
1890}
1891
1892impl Deref for PolicyId {
1893 type Target = str;
1894 fn deref(&self) -> &str {
1895 &self.0
1896 }
1897}
1898
1899impl fmt::Display for PolicyId {
1900 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1901 f.write_str(&self.0)
1902 }
1903}
1904
1905impl From<String> for PolicyId {
1906 fn from(s: String) -> Self {
1907 Self(s)
1908 }
1909}
1910
1911impl From<&str> for PolicyId {
1912 fn from(s: &str) -> Self {
1913 Self(s.to_string())
1914 }
1915}
1916
1917impl PartialEq<str> for PolicyId {
1918 fn eq(&self, other: &str) -> bool {
1919 self.0 == other
1920 }
1921}
1922
1923impl PartialEq<&str> for PolicyId {
1924 fn eq(&self, other: &&str) -> bool {
1925 self.0 == *other
1926 }
1927}
1928
1929#[cfg(test)]
1930#[allow(clippy::disallowed_methods)]
1931mod tests {
1932 use super::*;
1933 use crate::AttestationBuilder;
1934
1935 #[test]
1940 fn capability_serializes_to_snake_case() {
1941 assert_eq!(
1942 serde_json::to_string(&Capability::sign_commit()).unwrap(),
1943 r#""sign_commit""#
1944 );
1945 assert_eq!(
1946 serde_json::to_string(&Capability::sign_release()).unwrap(),
1947 r#""sign_release""#
1948 );
1949 assert_eq!(
1950 serde_json::to_string(&Capability::manage_members()).unwrap(),
1951 r#""manage_members""#
1952 );
1953 assert_eq!(
1954 serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1955 r#""rotate_keys""#
1956 );
1957 }
1958
1959 #[test]
1960 fn capability_deserializes_from_snake_case() {
1961 assert_eq!(
1962 serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1963 Capability::sign_commit()
1964 );
1965 assert_eq!(
1966 serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1967 Capability::sign_release()
1968 );
1969 assert_eq!(
1970 serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1971 Capability::manage_members()
1972 );
1973 assert_eq!(
1974 serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1975 Capability::rotate_keys()
1976 );
1977 }
1978
1979 #[test]
1980 fn capability_custom_serializes_as_string() {
1981 let cap = Capability::parse("acme:deploy").unwrap();
1982 assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1983 }
1984
1985 #[test]
1986 fn capability_custom_deserializes_unknown_strings() {
1987 let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1989 assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1990 }
1991
1992 #[test]
1997 fn capability_parse_accepts_valid_strings() {
1998 assert!(Capability::parse("deploy").is_ok());
1999 assert!(Capability::parse("acme:deploy").is_ok());
2000 assert!(Capability::parse("my-custom-cap").is_ok());
2001 assert!(Capability::parse("org:team:action").is_ok());
2002 assert!(Capability::parse("with_underscore").is_ok()); }
2004
2005 #[test]
2006 fn capability_parse_rejects_invalid_strings() {
2007 assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
2009
2010 assert!(matches!(
2012 Capability::parse(&"a".repeat(65)),
2013 Err(CapabilityError::TooLong(65))
2014 ));
2015
2016 assert!(matches!(
2018 Capability::parse("has spaces"),
2019 Err(CapabilityError::InvalidChars(_))
2020 ));
2021 assert!(matches!(
2022 Capability::parse("has.dot"),
2023 Err(CapabilityError::InvalidChars(_))
2024 ));
2025 }
2026
2027 #[test]
2028 fn capability_parse_rejects_reserved_namespace() {
2029 assert!(matches!(
2030 Capability::parse("auths:custom"),
2031 Err(CapabilityError::ReservedNamespace)
2032 ));
2033 assert!(matches!(
2034 Capability::parse("auths:sign_commit"),
2035 Err(CapabilityError::ReservedNamespace)
2036 ));
2037 }
2038
2039 #[test]
2040 fn capability_parse_normalizes_to_lowercase() {
2041 let cap = Capability::parse("DEPLOY").unwrap();
2042 assert_eq!(cap.as_str(), "deploy");
2043
2044 let cap = Capability::parse("ACME:Deploy").unwrap();
2045 assert_eq!(cap.as_str(), "acme:deploy");
2046 }
2047
2048 #[test]
2049 fn capability_parse_trims_whitespace() {
2050 let cap = Capability::parse(" deploy ").unwrap();
2051 assert_eq!(cap.as_str(), "deploy");
2052 }
2053
2054 #[test]
2059 fn capability_is_hashable() {
2060 use std::collections::HashSet;
2061 let mut set = HashSet::new();
2062 set.insert(Capability::sign_commit());
2063 set.insert(Capability::sign_release());
2064 set.insert(Capability::parse("test").unwrap());
2065 assert_eq!(set.len(), 3);
2066 assert!(set.contains(&Capability::sign_commit()));
2067 }
2068
2069 #[test]
2070 fn capability_equality_with_different_construction_paths() {
2071 let from_constructor = Capability::sign_commit();
2073 let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
2074 assert_eq!(from_constructor, from_deser);
2075
2076 let from_parse = Capability::parse("acme:deploy").unwrap();
2078 let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
2079 assert_eq!(from_parse, from_deser);
2080 }
2081
2082 #[test]
2087 fn capability_display_matches_canonical_form() {
2088 assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
2089 assert_eq!(Capability::sign_release().to_string(), "sign_release");
2090 assert_eq!(Capability::manage_members().to_string(), "manage_members");
2091 assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
2092 assert_eq!(
2093 Capability::parse("acme:deploy").unwrap().to_string(),
2094 "acme:deploy"
2095 );
2096 }
2097
2098 #[test]
2099 fn capability_as_str_returns_canonical_form() {
2100 assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
2101 assert_eq!(Capability::sign_release().as_str(), "sign_release");
2102 assert_eq!(Capability::manage_members().as_str(), "manage_members");
2103 assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
2104 assert_eq!(
2105 Capability::parse("acme:deploy").unwrap().as_str(),
2106 "acme:deploy"
2107 );
2108 }
2109
2110 #[test]
2111 fn capability_is_well_known() {
2112 assert!(Capability::sign_commit().is_well_known());
2113 assert!(Capability::sign_release().is_well_known());
2114 assert!(Capability::manage_members().is_well_known());
2115 assert!(Capability::rotate_keys().is_well_known());
2116 assert!(!Capability::parse("custom").unwrap().is_well_known());
2117 }
2118
2119 #[test]
2120 fn capability_namespace() {
2121 assert_eq!(
2122 Capability::parse("acme:deploy").unwrap().namespace(),
2123 Some("acme")
2124 );
2125 assert_eq!(
2126 Capability::parse("org:team:action").unwrap().namespace(),
2127 Some("org")
2128 );
2129 assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
2130 }
2131
2132 #[test]
2137 fn capability_vec_serializes_as_array() {
2138 let caps = vec![Capability::sign_commit(), Capability::sign_release()];
2139 let json = serde_json::to_string(&caps).unwrap();
2140 assert_eq!(json, r#"["sign_commit","sign_release"]"#);
2141 }
2142
2143 #[test]
2144 fn capability_vec_deserializes_from_array() {
2145 let json = r#"["sign_commit","manage_members","custom-cap"]"#;
2146 let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
2147 assert_eq!(caps.len(), 3);
2148 assert_eq!(caps[0], Capability::sign_commit());
2149 assert_eq!(caps[1], Capability::manage_members());
2150 assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
2151 }
2152
2153 #[test]
2158 fn capability_serde_roundtrip_well_known() {
2159 let caps = vec![
2160 Capability::sign_commit(),
2161 Capability::sign_release(),
2162 Capability::manage_members(),
2163 Capability::rotate_keys(),
2164 ];
2165 for cap in caps {
2166 let json = serde_json::to_string(&cap).unwrap();
2167 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2168 assert_eq!(cap, roundtrip);
2169 }
2170 }
2171
2172 #[test]
2173 fn capability_serde_roundtrip_custom() {
2174 let caps = vec![
2175 Capability::parse("deploy").unwrap(),
2176 Capability::parse("acme:deploy").unwrap(),
2177 Capability::parse("org:team:action").unwrap(),
2178 ];
2179 for cap in caps {
2180 let json = serde_json::to_string(&cap).unwrap();
2181 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2182 assert_eq!(cap, roundtrip);
2183 }
2184 }
2185
2186 #[test]
2189 fn attestation_old_json_without_delegated_by_deserializes() {
2190 let old_json = r#"{
2192 "version": 1,
2193 "rid": "test-rid",
2194 "issuer": "did:keri:Eissuer",
2195 "subject": "did:key:zSubject",
2196 "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
2197 "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2198 "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2199 "revoked_at": null,
2200 "timestamp": null
2201 }"#;
2202
2203 let att: Attestation = serde_json::from_str(old_json).unwrap();
2204
2205 assert_eq!(att.delegated_by, None);
2206 }
2207
2208 #[test]
2209 fn attestation_delegated_by_serializes_correctly() {
2210 let att = AttestationBuilder::default()
2211 .rid("test-rid")
2212 .issuer("did:keri:Eissuer")
2213 .subject("did:key:zSubject")
2214 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
2215 .build();
2216
2217 let json = serde_json::to_string(&att).unwrap();
2218 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2219
2220 assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
2221 }
2222
2223 #[test]
2224 fn attestation_omits_delegated_by_when_absent() {
2225 let att = AttestationBuilder::default()
2226 .rid("test-rid")
2227 .issuer("did:keri:Eissuer")
2228 .subject("did:key:zSubject")
2229 .build();
2230
2231 let json = serde_json::to_string(&att).unwrap();
2232 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2233
2234 assert!(parsed.get("delegated_by").is_none());
2235 }
2236
2237 #[test]
2238 fn attestation_delegated_by_roundtrips() {
2239 let original = AttestationBuilder::default()
2240 .rid("test-rid")
2241 .issuer("did:keri:Eissuer")
2242 .subject("did:key:zSubject")
2243 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
2244 .build();
2245
2246 let json = serde_json::to_string(&original).unwrap();
2247 let deserialized: Attestation = serde_json::from_str(&json).unwrap();
2248
2249 assert_eq!(original.delegated_by, deserialized.delegated_by);
2250 }
2251
2252 #[test]
2255 fn threshold_policy_new_creates_valid_policy() {
2256 let policy = ThresholdPolicy::new(
2257 2,
2258 vec![
2259 "did:key:alice".to_string(),
2260 "did:key:bob".to_string(),
2261 "did:key:carol".to_string(),
2262 ],
2263 "test-policy".to_string(),
2264 );
2265
2266 assert_eq!(policy.threshold, 2);
2267 assert_eq!(policy.signers.len(), 3);
2268 assert_eq!(policy.policy_id, "test-policy");
2269 assert!(policy.scope.is_none());
2270 assert!(policy.ceremony_endpoint.is_none());
2271 }
2272
2273 #[test]
2274 fn threshold_policy_is_valid_checks_constraints() {
2275 let valid = ThresholdPolicy::new(
2277 2,
2278 vec!["a".to_string(), "b".to_string(), "c".to_string()],
2279 "policy".to_string(),
2280 );
2281 assert!(valid.is_valid());
2282
2283 let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
2285 assert!(!zero_threshold.is_valid());
2286
2287 let too_high = ThresholdPolicy::new(
2289 3,
2290 vec!["a".to_string(), "b".to_string()],
2291 "policy".to_string(),
2292 );
2293 assert!(!too_high.is_valid());
2294
2295 let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
2297 assert!(!no_signers.is_valid());
2298
2299 let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
2301 assert!(!no_id.is_valid());
2302 }
2303
2304 #[test]
2305 fn threshold_policy_m_of_n_returns_correct_values() {
2306 let policy = ThresholdPolicy::new(
2307 2,
2308 vec!["a".to_string(), "b".to_string(), "c".to_string()],
2309 "policy".to_string(),
2310 );
2311 let (m, n) = policy.m_of_n();
2312 assert_eq!(m, 2);
2313 assert_eq!(n, 3);
2314 }
2315
2316 #[test]
2317 fn threshold_policy_serializes_correctly() {
2318 let mut policy = ThresholdPolicy::new(
2319 2,
2320 vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
2321 "release-policy".to_string(),
2322 );
2323 policy.scope = Some(Capability::sign_release());
2324 policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
2325
2326 let json = serde_json::to_string(&policy).unwrap();
2327 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2328
2329 assert_eq!(parsed["threshold"], 2);
2330 assert_eq!(parsed["signers"][0], "did:key:alice");
2331 assert_eq!(parsed["policy_id"], "release-policy");
2332 assert_eq!(parsed["scope"], "sign_release");
2333 assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
2334 }
2335
2336 #[test]
2337 fn threshold_policy_without_optional_fields_omits_them() {
2338 let policy =
2339 ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
2340
2341 let json = serde_json::to_string(&policy).unwrap();
2342 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2343
2344 assert!(parsed.get("scope").is_none());
2345 assert!(parsed.get("ceremony_endpoint").is_none());
2346 }
2347
2348 #[test]
2349 fn threshold_policy_roundtrips() {
2350 let mut original = ThresholdPolicy::new(
2351 3,
2352 vec![
2353 "a".to_string(),
2354 "b".to_string(),
2355 "c".to_string(),
2356 "d".to_string(),
2357 ],
2358 "important-policy".to_string(),
2359 );
2360 original.scope = Some(Capability::rotate_keys());
2361
2362 let json = serde_json::to_string(&original).unwrap();
2363 let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
2364
2365 assert_eq!(original, deserialized);
2366 }
2367
2368 #[test]
2371 fn identity_bundle_serializes_correctly() {
2372 let bundle = IdentityBundle {
2373 identity_did: IdentityDID::new_unchecked("did:keri:test123"),
2374 public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
2375 curve: Default::default(),
2376 attestation_chain: vec![],
2377 kel: vec![],
2378 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2379 .unwrap()
2380 .with_timezone(&Utc),
2381 max_valid_for_secs: 86400,
2382 };
2383
2384 let json = serde_json::to_string(&bundle).unwrap();
2385 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2386
2387 assert_eq!(parsed["identity_did"], "did:keri:test123");
2388 assert_eq!(parsed["public_key_hex"], "aabbccdd");
2389 assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
2390 }
2391
2392 #[test]
2393 fn identity_bundle_deserializes_correctly() {
2394 let json = r#"{
2395 "identity_did": "did:keri:abc123",
2396 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
2397 "attestation_chain": [],
2398 "bundle_timestamp": "2099-01-01T00:00:00Z",
2399 "max_valid_for_secs": 86400
2400 }"#;
2401
2402 let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
2403
2404 assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
2405 assert_eq!(
2406 bundle.public_key_hex.as_str(),
2407 "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
2408 );
2409 assert!(bundle.attestation_chain.is_empty());
2410 }
2411
2412 #[test]
2413 fn identity_bundle_roundtrips() {
2414 let attestation = AttestationBuilder::default()
2415 .rid("test-rid")
2416 .issuer("did:keri:Eissuer")
2417 .subject("did:key:zSubject")
2418 .build();
2419
2420 let original = IdentityBundle {
2421 identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
2422 public_key_hex: PublicKeyHex::new_unchecked(
2423 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
2424 ),
2425 curve: Default::default(),
2426 attestation_chain: vec![attestation],
2427 kel: vec![],
2428 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2429 .unwrap()
2430 .with_timezone(&Utc),
2431 max_valid_for_secs: 86400,
2432 };
2433
2434 let json = serde_json::to_string(&original).unwrap();
2435 let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
2436
2437 assert_eq!(original.identity_did, deserialized.identity_did);
2438 assert_eq!(original.public_key_hex, deserialized.public_key_hex);
2439 assert_eq!(
2440 original.attestation_chain.len(),
2441 deserialized.attestation_chain.len()
2442 );
2443 }
2444}
2445
2446#[cfg(test)]
2447mod decode_public_key_tests {
2448 use super::*;
2449
2450 #[test]
2451 fn hex_ed25519_32_bytes() {
2452 let hex = "00".repeat(32);
2453 let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::Ed25519).unwrap();
2454 assert_eq!(pk.curve(), auths_crypto::CurveType::Ed25519);
2455 assert_eq!(pk.len(), 32);
2456 }
2457
2458 #[test]
2459 fn hex_p256_33_bytes_compressed() {
2460 let mut bytes = [0u8; 33];
2462 bytes[0] = 0x02;
2463 let hex = hex::encode(bytes);
2464 let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::P256).unwrap();
2465 assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2466 assert_eq!(pk.len(), 33);
2467 }
2468
2469 #[test]
2470 fn bytes_p256_65_uncompressed() {
2471 let mut bytes = [0u8; 65];
2472 bytes[0] = 0x04;
2473 let pk = decode_public_key_bytes(&bytes, auths_crypto::CurveType::P256).unwrap();
2474 assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2475 assert_eq!(pk.len(), 65);
2476 }
2477
2478 #[test]
2479 fn rejects_validation_error() {
2480 let err = decode_public_key_bytes(&[0u8; 50], auths_crypto::CurveType::P256).unwrap_err();
2481 assert!(matches!(err, PublicKeyDecodeError::Validation(_)));
2482 }
2483
2484 #[test]
2485 fn rejects_malformed_hex() {
2486 let err = decode_public_key_hex("zz", auths_crypto::CurveType::P256).unwrap_err();
2487 assert!(matches!(err, PublicKeyDecodeError::InvalidHex(_)));
2488 }
2489}