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 pub bundle_timestamp: DateTime<Utc>,
1217 pub max_valid_for_secs: u64,
1219}
1220
1221impl IdentityBundle {
1222 pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
1232 let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
1233 if age > self.max_valid_for_secs {
1234 return Err(AttestationError::BundleExpired {
1235 age_secs: age,
1236 max_secs: self.max_valid_for_secs,
1237 });
1238 }
1239 Ok(())
1240 }
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1246pub struct Attestation {
1247 pub version: u32,
1249 pub rid: ResourceId,
1251 pub issuer: CanonicalDid,
1253 pub subject: CanonicalDid,
1255 pub device_public_key: DevicePublicKey,
1257 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
1259 pub identity_signature: Ed25519Signature,
1260 pub device_signature: Ed25519Signature,
1262 #[serde(default, skip_serializing_if = "Option::is_none")]
1264 pub revoked_at: Option<DateTime<Utc>>,
1265 #[serde(skip_serializing_if = "Option::is_none")]
1267 pub expires_at: Option<DateTime<Utc>>,
1268 pub timestamp: Option<DateTime<Utc>>,
1270 #[serde(skip_serializing_if = "Option::is_none")]
1272 pub note: Option<String>,
1273 #[serde(skip_serializing_if = "Option::is_none")]
1275 pub payload: Option<Value>,
1276
1277 #[serde(skip_serializing_if = "Option::is_none")]
1279 pub commit_sha: Option<String>,
1280
1281 #[serde(skip_serializing_if = "Option::is_none")]
1283 pub commit_message: Option<String>,
1284
1285 #[serde(skip_serializing_if = "Option::is_none")]
1287 pub author: Option<String>,
1288
1289 #[serde(skip_serializing_if = "Option::is_none")]
1291 pub oidc_binding: Option<OidcBinding>,
1292
1293 #[serde(default, skip_serializing_if = "Option::is_none")]
1295 pub delegated_by: Option<CanonicalDid>,
1296
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub signer_type: Option<SignerType>,
1301
1302 #[serde(default, skip_serializing_if = "Option::is_none")]
1305 pub environment_claim: Option<Value>,
1306}
1307
1308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1315pub struct OidcBinding {
1316 pub issuer: String,
1318 pub subject: String,
1320 pub audience: String,
1322 pub token_exp: i64,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1326 pub platform: Option<String>,
1327 #[serde(default, skip_serializing_if = "Option::is_none")]
1329 pub jti: Option<String>,
1330 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
1333}
1334
1335#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1340#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1341#[non_exhaustive]
1342pub enum SignerType {
1343 Human,
1345 Agent,
1347 Workload,
1349}
1350
1351#[derive(Debug, Clone, Serialize)]
1361pub struct VerifiedAttestation(Attestation);
1362
1363impl VerifiedAttestation {
1364 pub fn inner(&self) -> &Attestation {
1366 &self.0
1367 }
1368
1369 pub fn into_inner(self) -> Attestation {
1371 self.0
1372 }
1373
1374 #[doc(hidden)]
1380 pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
1381 Self(attestation)
1382 }
1383
1384 pub(crate) fn from_verified(attestation: Attestation) -> Self {
1385 Self(attestation)
1386 }
1387}
1388
1389impl std::ops::Deref for VerifiedAttestation {
1390 type Target = Attestation;
1391
1392 fn deref(&self) -> &Attestation {
1393 &self.0
1394 }
1395}
1396
1397#[derive(Serialize, Debug)]
1399pub struct CanonicalAttestationData<'a> {
1400 pub version: u32,
1402 pub rid: &'a str,
1404 pub issuer: &'a CanonicalDid,
1406 pub subject: &'a CanonicalDid,
1408 #[serde(with = "hex::serde")]
1410 pub device_public_key: &'a [u8],
1411 pub payload: &'a Option<Value>,
1413 pub timestamp: &'a Option<DateTime<Utc>>,
1415 pub expires_at: &'a Option<DateTime<Utc>>,
1417 pub revoked_at: &'a Option<DateTime<Utc>>,
1419 pub note: &'a Option<String>,
1421
1422 #[serde(skip_serializing_if = "Option::is_none")]
1424 pub delegated_by: Option<&'a CanonicalDid>,
1425 #[serde(skip_serializing_if = "Option::is_none")]
1427 pub signer_type: Option<&'a SignerType>,
1428 #[serde(skip_serializing_if = "Option::is_none")]
1430 pub commit_sha: Option<&'a str>,
1431}
1432
1433pub fn canonicalize_attestation_data(
1438 data: &CanonicalAttestationData,
1439) -> Result<Vec<u8>, AttestationError> {
1440 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
1441 AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
1442 })?;
1443 debug!(
1444 "Generated canonical data (standard): {}",
1445 canonical_json_string
1446 );
1447 Ok(canonical_json_string.into_bytes())
1448}
1449
1450impl Attestation {
1451 pub fn is_revoked(&self) -> bool {
1453 self.revoked_at.is_some()
1454 }
1455
1456 pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
1460 if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
1461 return Err(AttestationError::InputTooLarge(format!(
1462 "attestation JSON is {} bytes, max {}",
1463 json_bytes.len(),
1464 MAX_ATTESTATION_JSON_SIZE
1465 )));
1466 }
1467 serde_json::from_slice(json_bytes)
1468 .map_err(|e| AttestationError::SerializationError(e.to_string()))
1469 }
1470
1471 pub fn canonical_data(&self) -> CanonicalAttestationData<'_> {
1482 CanonicalAttestationData {
1483 version: self.version,
1484 rid: &self.rid,
1485 issuer: &self.issuer,
1486 subject: &self.subject,
1487 device_public_key: self.device_public_key.as_bytes(),
1488 payload: &self.payload,
1489 timestamp: &self.timestamp,
1490 expires_at: &self.expires_at,
1491 revoked_at: &self.revoked_at,
1492 note: &self.note,
1493 delegated_by: self.delegated_by.as_ref(),
1494 signer_type: self.signer_type.as_ref(),
1495 commit_sha: self.commit_sha.as_deref(),
1496 }
1497 }
1498
1499 pub fn to_debug_string(&self) -> String {
1501 format!(
1502 "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
1503 self.rid,
1504 self.issuer,
1505 self.subject, hex::encode(self.device_public_key.as_bytes()),
1507 hex::encode(self.identity_signature.as_bytes()),
1508 hex::encode(self.device_signature.as_bytes()),
1509 self.revoked_at,
1510 self.expires_at,
1511 self.note
1512 )
1513 }
1514}
1515
1516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1583#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1584pub struct ThresholdPolicy {
1585 pub threshold: u8,
1587
1588 pub signers: Vec<String>,
1590
1591 pub policy_id: PolicyId,
1593
1594 #[serde(default, skip_serializing_if = "Option::is_none")]
1596 pub scope: Option<Capability>,
1597
1598 #[serde(default, skip_serializing_if = "Option::is_none")]
1600 pub ceremony_endpoint: Option<String>,
1601}
1602
1603impl ThresholdPolicy {
1604 pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1606 Self {
1607 threshold,
1608 signers,
1609 policy_id: policy_id.into(),
1610 scope: None,
1611 ceremony_endpoint: None,
1612 }
1613 }
1614
1615 pub fn is_valid(&self) -> bool {
1617 if self.threshold < 1 {
1619 return false;
1620 }
1621 if self.threshold as usize > self.signers.len() {
1623 return false;
1624 }
1625 if self.signers.is_empty() {
1627 return false;
1628 }
1629 if self.policy_id.is_empty() {
1631 return false;
1632 }
1633 true
1634 }
1635
1636 pub fn m_of_n(&self) -> (u8, usize) {
1638 (self.threshold, self.signers.len())
1639 }
1640}
1641
1642#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1648pub enum CommitOidError {
1649 #[error("commit OID is empty")]
1651 Empty,
1652 #[error("expected 40 or 64 hex chars, got {0}")]
1654 InvalidLength(usize),
1655 #[error("invalid hex character in commit OID")]
1657 InvalidHex,
1658}
1659
1660#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1664#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1665#[repr(transparent)]
1666#[serde(try_from = "String")]
1667pub struct CommitOid(String);
1668
1669impl CommitOid {
1670 pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1680 let s = raw.trim().to_lowercase();
1681 if s.is_empty() {
1682 return Err(CommitOidError::Empty);
1683 }
1684 if s.len() != 40 && s.len() != 64 {
1685 return Err(CommitOidError::InvalidLength(s.len()));
1686 }
1687 if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1688 return Err(CommitOidError::InvalidHex);
1689 }
1690 Ok(Self(s))
1691 }
1692
1693 pub fn new_unchecked(s: impl Into<String>) -> Self {
1697 Self(s.into())
1698 }
1699
1700 pub fn as_str(&self) -> &str {
1702 &self.0
1703 }
1704
1705 pub fn into_inner(self) -> String {
1707 self.0
1708 }
1709}
1710
1711impl fmt::Display for CommitOid {
1712 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1713 f.write_str(&self.0)
1714 }
1715}
1716
1717impl AsRef<str> for CommitOid {
1718 fn as_ref(&self) -> &str {
1719 &self.0
1720 }
1721}
1722
1723impl TryFrom<String> for CommitOid {
1724 type Error = CommitOidError;
1725 fn try_from(s: String) -> Result<Self, Self::Error> {
1726 Self::parse(&s)
1727 }
1728}
1729
1730impl TryFrom<&str> for CommitOid {
1731 type Error = CommitOidError;
1732 fn try_from(s: &str) -> Result<Self, Self::Error> {
1733 Self::parse(s)
1734 }
1735}
1736
1737impl FromStr for CommitOid {
1738 type Err = CommitOidError;
1739 fn from_str(s: &str) -> Result<Self, Self::Err> {
1740 Self::parse(s)
1741 }
1742}
1743
1744impl From<CommitOid> for String {
1745 fn from(oid: CommitOid) -> Self {
1746 oid.0
1747 }
1748}
1749
1750impl<'de> Deserialize<'de> for CommitOid {
1751 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1752 let s = String::deserialize(d)?;
1753 Self::parse(&s).map_err(serde::de::Error::custom)
1754 }
1755}
1756
1757#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1763pub enum PublicKeyHexError {
1764 #[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
1766 InvalidLength(usize),
1767 #[error("invalid hex: {0}")]
1769 InvalidHex(String),
1770}
1771
1772#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1774#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1775#[repr(transparent)]
1776#[serde(try_from = "String")]
1777pub struct PublicKeyHex(String);
1778
1779impl PublicKeyHex {
1780 pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1790 let s = raw.trim().to_lowercase();
1791 let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1792 if bytes.len() != 32 && bytes.len() != 33 {
1793 return Err(PublicKeyHexError::InvalidLength(s.len()));
1794 }
1795 Ok(Self(s))
1796 }
1797
1798 pub fn new_unchecked(s: impl Into<String>) -> Self {
1802 Self(s.into())
1803 }
1804
1805 pub fn as_str(&self) -> &str {
1807 &self.0
1808 }
1809
1810 pub fn into_inner(self) -> String {
1812 self.0
1813 }
1814}
1815
1816impl fmt::Display for PublicKeyHex {
1817 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1818 f.write_str(&self.0)
1819 }
1820}
1821
1822impl AsRef<str> for PublicKeyHex {
1823 fn as_ref(&self) -> &str {
1824 &self.0
1825 }
1826}
1827
1828impl TryFrom<String> for PublicKeyHex {
1829 type Error = PublicKeyHexError;
1830 fn try_from(s: String) -> Result<Self, Self::Error> {
1831 Self::parse(&s)
1832 }
1833}
1834
1835impl TryFrom<&str> for PublicKeyHex {
1836 type Error = PublicKeyHexError;
1837 fn try_from(s: &str) -> Result<Self, Self::Error> {
1838 Self::parse(s)
1839 }
1840}
1841
1842impl FromStr for PublicKeyHex {
1843 type Err = PublicKeyHexError;
1844 fn from_str(s: &str) -> Result<Self, Self::Err> {
1845 Self::parse(s)
1846 }
1847}
1848
1849impl From<PublicKeyHex> for String {
1850 fn from(pk: PublicKeyHex) -> Self {
1851 pk.0
1852 }
1853}
1854
1855impl<'de> Deserialize<'de> for PublicKeyHex {
1856 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1857 let s = String::deserialize(d)?;
1858 Self::parse(&s).map_err(serde::de::Error::custom)
1859 }
1860}
1861
1862#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1871#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1872#[serde(transparent)]
1873pub struct PolicyId(String);
1874
1875impl PolicyId {
1876 pub fn new(s: impl Into<String>) -> Self {
1878 Self(s.into())
1879 }
1880
1881 pub fn as_str(&self) -> &str {
1883 &self.0
1884 }
1885}
1886
1887impl Deref for PolicyId {
1888 type Target = str;
1889 fn deref(&self) -> &str {
1890 &self.0
1891 }
1892}
1893
1894impl fmt::Display for PolicyId {
1895 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1896 f.write_str(&self.0)
1897 }
1898}
1899
1900impl From<String> for PolicyId {
1901 fn from(s: String) -> Self {
1902 Self(s)
1903 }
1904}
1905
1906impl From<&str> for PolicyId {
1907 fn from(s: &str) -> Self {
1908 Self(s.to_string())
1909 }
1910}
1911
1912impl PartialEq<str> for PolicyId {
1913 fn eq(&self, other: &str) -> bool {
1914 self.0 == other
1915 }
1916}
1917
1918impl PartialEq<&str> for PolicyId {
1919 fn eq(&self, other: &&str) -> bool {
1920 self.0 == *other
1921 }
1922}
1923
1924#[cfg(test)]
1925#[allow(clippy::disallowed_methods)]
1926mod tests {
1927 use super::*;
1928 use crate::AttestationBuilder;
1929
1930 #[test]
1935 fn capability_serializes_to_snake_case() {
1936 assert_eq!(
1937 serde_json::to_string(&Capability::sign_commit()).unwrap(),
1938 r#""sign_commit""#
1939 );
1940 assert_eq!(
1941 serde_json::to_string(&Capability::sign_release()).unwrap(),
1942 r#""sign_release""#
1943 );
1944 assert_eq!(
1945 serde_json::to_string(&Capability::manage_members()).unwrap(),
1946 r#""manage_members""#
1947 );
1948 assert_eq!(
1949 serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1950 r#""rotate_keys""#
1951 );
1952 }
1953
1954 #[test]
1955 fn capability_deserializes_from_snake_case() {
1956 assert_eq!(
1957 serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1958 Capability::sign_commit()
1959 );
1960 assert_eq!(
1961 serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1962 Capability::sign_release()
1963 );
1964 assert_eq!(
1965 serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1966 Capability::manage_members()
1967 );
1968 assert_eq!(
1969 serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1970 Capability::rotate_keys()
1971 );
1972 }
1973
1974 #[test]
1975 fn capability_custom_serializes_as_string() {
1976 let cap = Capability::parse("acme:deploy").unwrap();
1977 assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1978 }
1979
1980 #[test]
1981 fn capability_custom_deserializes_unknown_strings() {
1982 let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1984 assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1985 }
1986
1987 #[test]
1992 fn capability_parse_accepts_valid_strings() {
1993 assert!(Capability::parse("deploy").is_ok());
1994 assert!(Capability::parse("acme:deploy").is_ok());
1995 assert!(Capability::parse("my-custom-cap").is_ok());
1996 assert!(Capability::parse("org:team:action").is_ok());
1997 assert!(Capability::parse("with_underscore").is_ok()); }
1999
2000 #[test]
2001 fn capability_parse_rejects_invalid_strings() {
2002 assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
2004
2005 assert!(matches!(
2007 Capability::parse(&"a".repeat(65)),
2008 Err(CapabilityError::TooLong(65))
2009 ));
2010
2011 assert!(matches!(
2013 Capability::parse("has spaces"),
2014 Err(CapabilityError::InvalidChars(_))
2015 ));
2016 assert!(matches!(
2017 Capability::parse("has.dot"),
2018 Err(CapabilityError::InvalidChars(_))
2019 ));
2020 }
2021
2022 #[test]
2023 fn capability_parse_rejects_reserved_namespace() {
2024 assert!(matches!(
2025 Capability::parse("auths:custom"),
2026 Err(CapabilityError::ReservedNamespace)
2027 ));
2028 assert!(matches!(
2029 Capability::parse("auths:sign_commit"),
2030 Err(CapabilityError::ReservedNamespace)
2031 ));
2032 }
2033
2034 #[test]
2035 fn capability_parse_normalizes_to_lowercase() {
2036 let cap = Capability::parse("DEPLOY").unwrap();
2037 assert_eq!(cap.as_str(), "deploy");
2038
2039 let cap = Capability::parse("ACME:Deploy").unwrap();
2040 assert_eq!(cap.as_str(), "acme:deploy");
2041 }
2042
2043 #[test]
2044 fn capability_parse_trims_whitespace() {
2045 let cap = Capability::parse(" deploy ").unwrap();
2046 assert_eq!(cap.as_str(), "deploy");
2047 }
2048
2049 #[test]
2054 fn capability_is_hashable() {
2055 use std::collections::HashSet;
2056 let mut set = HashSet::new();
2057 set.insert(Capability::sign_commit());
2058 set.insert(Capability::sign_release());
2059 set.insert(Capability::parse("test").unwrap());
2060 assert_eq!(set.len(), 3);
2061 assert!(set.contains(&Capability::sign_commit()));
2062 }
2063
2064 #[test]
2065 fn capability_equality_with_different_construction_paths() {
2066 let from_constructor = Capability::sign_commit();
2068 let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
2069 assert_eq!(from_constructor, from_deser);
2070
2071 let from_parse = Capability::parse("acme:deploy").unwrap();
2073 let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
2074 assert_eq!(from_parse, from_deser);
2075 }
2076
2077 #[test]
2082 fn capability_display_matches_canonical_form() {
2083 assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
2084 assert_eq!(Capability::sign_release().to_string(), "sign_release");
2085 assert_eq!(Capability::manage_members().to_string(), "manage_members");
2086 assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
2087 assert_eq!(
2088 Capability::parse("acme:deploy").unwrap().to_string(),
2089 "acme:deploy"
2090 );
2091 }
2092
2093 #[test]
2094 fn capability_as_str_returns_canonical_form() {
2095 assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
2096 assert_eq!(Capability::sign_release().as_str(), "sign_release");
2097 assert_eq!(Capability::manage_members().as_str(), "manage_members");
2098 assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
2099 assert_eq!(
2100 Capability::parse("acme:deploy").unwrap().as_str(),
2101 "acme:deploy"
2102 );
2103 }
2104
2105 #[test]
2106 fn capability_is_well_known() {
2107 assert!(Capability::sign_commit().is_well_known());
2108 assert!(Capability::sign_release().is_well_known());
2109 assert!(Capability::manage_members().is_well_known());
2110 assert!(Capability::rotate_keys().is_well_known());
2111 assert!(!Capability::parse("custom").unwrap().is_well_known());
2112 }
2113
2114 #[test]
2115 fn capability_namespace() {
2116 assert_eq!(
2117 Capability::parse("acme:deploy").unwrap().namespace(),
2118 Some("acme")
2119 );
2120 assert_eq!(
2121 Capability::parse("org:team:action").unwrap().namespace(),
2122 Some("org")
2123 );
2124 assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
2125 }
2126
2127 #[test]
2132 fn capability_vec_serializes_as_array() {
2133 let caps = vec![Capability::sign_commit(), Capability::sign_release()];
2134 let json = serde_json::to_string(&caps).unwrap();
2135 assert_eq!(json, r#"["sign_commit","sign_release"]"#);
2136 }
2137
2138 #[test]
2139 fn capability_vec_deserializes_from_array() {
2140 let json = r#"["sign_commit","manage_members","custom-cap"]"#;
2141 let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
2142 assert_eq!(caps.len(), 3);
2143 assert_eq!(caps[0], Capability::sign_commit());
2144 assert_eq!(caps[1], Capability::manage_members());
2145 assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
2146 }
2147
2148 #[test]
2153 fn capability_serde_roundtrip_well_known() {
2154 let caps = vec![
2155 Capability::sign_commit(),
2156 Capability::sign_release(),
2157 Capability::manage_members(),
2158 Capability::rotate_keys(),
2159 ];
2160 for cap in caps {
2161 let json = serde_json::to_string(&cap).unwrap();
2162 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2163 assert_eq!(cap, roundtrip);
2164 }
2165 }
2166
2167 #[test]
2168 fn capability_serde_roundtrip_custom() {
2169 let caps = vec![
2170 Capability::parse("deploy").unwrap(),
2171 Capability::parse("acme:deploy").unwrap(),
2172 Capability::parse("org:team:action").unwrap(),
2173 ];
2174 for cap in caps {
2175 let json = serde_json::to_string(&cap).unwrap();
2176 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2177 assert_eq!(cap, roundtrip);
2178 }
2179 }
2180
2181 #[test]
2184 fn attestation_old_json_without_delegated_by_deserializes() {
2185 let old_json = r#"{
2187 "version": 1,
2188 "rid": "test-rid",
2189 "issuer": "did:keri:Eissuer",
2190 "subject": "did:key:zSubject",
2191 "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
2192 "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2193 "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2194 "revoked_at": null,
2195 "timestamp": null
2196 }"#;
2197
2198 let att: Attestation = serde_json::from_str(old_json).unwrap();
2199
2200 assert_eq!(att.delegated_by, None);
2201 }
2202
2203 #[test]
2204 fn attestation_delegated_by_serializes_correctly() {
2205 let att = AttestationBuilder::default()
2206 .rid("test-rid")
2207 .issuer("did:keri:Eissuer")
2208 .subject("did:key:zSubject")
2209 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
2210 .build();
2211
2212 let json = serde_json::to_string(&att).unwrap();
2213 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2214
2215 assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
2216 }
2217
2218 #[test]
2219 fn attestation_omits_delegated_by_when_absent() {
2220 let att = AttestationBuilder::default()
2221 .rid("test-rid")
2222 .issuer("did:keri:Eissuer")
2223 .subject("did:key:zSubject")
2224 .build();
2225
2226 let json = serde_json::to_string(&att).unwrap();
2227 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2228
2229 assert!(parsed.get("delegated_by").is_none());
2230 }
2231
2232 #[test]
2233 fn attestation_delegated_by_roundtrips() {
2234 let original = AttestationBuilder::default()
2235 .rid("test-rid")
2236 .issuer("did:keri:Eissuer")
2237 .subject("did:key:zSubject")
2238 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
2239 .build();
2240
2241 let json = serde_json::to_string(&original).unwrap();
2242 let deserialized: Attestation = serde_json::from_str(&json).unwrap();
2243
2244 assert_eq!(original.delegated_by, deserialized.delegated_by);
2245 }
2246
2247 #[test]
2250 fn threshold_policy_new_creates_valid_policy() {
2251 let policy = ThresholdPolicy::new(
2252 2,
2253 vec![
2254 "did:key:alice".to_string(),
2255 "did:key:bob".to_string(),
2256 "did:key:carol".to_string(),
2257 ],
2258 "test-policy".to_string(),
2259 );
2260
2261 assert_eq!(policy.threshold, 2);
2262 assert_eq!(policy.signers.len(), 3);
2263 assert_eq!(policy.policy_id, "test-policy");
2264 assert!(policy.scope.is_none());
2265 assert!(policy.ceremony_endpoint.is_none());
2266 }
2267
2268 #[test]
2269 fn threshold_policy_is_valid_checks_constraints() {
2270 let valid = ThresholdPolicy::new(
2272 2,
2273 vec!["a".to_string(), "b".to_string(), "c".to_string()],
2274 "policy".to_string(),
2275 );
2276 assert!(valid.is_valid());
2277
2278 let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
2280 assert!(!zero_threshold.is_valid());
2281
2282 let too_high = ThresholdPolicy::new(
2284 3,
2285 vec!["a".to_string(), "b".to_string()],
2286 "policy".to_string(),
2287 );
2288 assert!(!too_high.is_valid());
2289
2290 let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
2292 assert!(!no_signers.is_valid());
2293
2294 let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
2296 assert!(!no_id.is_valid());
2297 }
2298
2299 #[test]
2300 fn threshold_policy_m_of_n_returns_correct_values() {
2301 let policy = ThresholdPolicy::new(
2302 2,
2303 vec!["a".to_string(), "b".to_string(), "c".to_string()],
2304 "policy".to_string(),
2305 );
2306 let (m, n) = policy.m_of_n();
2307 assert_eq!(m, 2);
2308 assert_eq!(n, 3);
2309 }
2310
2311 #[test]
2312 fn threshold_policy_serializes_correctly() {
2313 let mut policy = ThresholdPolicy::new(
2314 2,
2315 vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
2316 "release-policy".to_string(),
2317 );
2318 policy.scope = Some(Capability::sign_release());
2319 policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
2320
2321 let json = serde_json::to_string(&policy).unwrap();
2322 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2323
2324 assert_eq!(parsed["threshold"], 2);
2325 assert_eq!(parsed["signers"][0], "did:key:alice");
2326 assert_eq!(parsed["policy_id"], "release-policy");
2327 assert_eq!(parsed["scope"], "sign_release");
2328 assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
2329 }
2330
2331 #[test]
2332 fn threshold_policy_without_optional_fields_omits_them() {
2333 let policy =
2334 ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
2335
2336 let json = serde_json::to_string(&policy).unwrap();
2337 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2338
2339 assert!(parsed.get("scope").is_none());
2340 assert!(parsed.get("ceremony_endpoint").is_none());
2341 }
2342
2343 #[test]
2344 fn threshold_policy_roundtrips() {
2345 let mut original = ThresholdPolicy::new(
2346 3,
2347 vec![
2348 "a".to_string(),
2349 "b".to_string(),
2350 "c".to_string(),
2351 "d".to_string(),
2352 ],
2353 "important-policy".to_string(),
2354 );
2355 original.scope = Some(Capability::rotate_keys());
2356
2357 let json = serde_json::to_string(&original).unwrap();
2358 let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
2359
2360 assert_eq!(original, deserialized);
2361 }
2362
2363 #[test]
2366 fn identity_bundle_serializes_correctly() {
2367 let bundle = IdentityBundle {
2368 identity_did: IdentityDID::new_unchecked("did:keri:test123"),
2369 public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
2370 curve: Default::default(),
2371 attestation_chain: vec![],
2372 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2373 .unwrap()
2374 .with_timezone(&Utc),
2375 max_valid_for_secs: 86400,
2376 };
2377
2378 let json = serde_json::to_string(&bundle).unwrap();
2379 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2380
2381 assert_eq!(parsed["identity_did"], "did:keri:test123");
2382 assert_eq!(parsed["public_key_hex"], "aabbccdd");
2383 assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
2384 }
2385
2386 #[test]
2387 fn identity_bundle_deserializes_correctly() {
2388 let json = r#"{
2389 "identity_did": "did:keri:abc123",
2390 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
2391 "attestation_chain": [],
2392 "bundle_timestamp": "2099-01-01T00:00:00Z",
2393 "max_valid_for_secs": 86400
2394 }"#;
2395
2396 let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
2397
2398 assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
2399 assert_eq!(
2400 bundle.public_key_hex.as_str(),
2401 "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
2402 );
2403 assert!(bundle.attestation_chain.is_empty());
2404 }
2405
2406 #[test]
2407 fn identity_bundle_roundtrips() {
2408 let attestation = AttestationBuilder::default()
2409 .rid("test-rid")
2410 .issuer("did:keri:Eissuer")
2411 .subject("did:key:zSubject")
2412 .build();
2413
2414 let original = IdentityBundle {
2415 identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
2416 public_key_hex: PublicKeyHex::new_unchecked(
2417 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
2418 ),
2419 curve: Default::default(),
2420 attestation_chain: vec![attestation],
2421 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2422 .unwrap()
2423 .with_timezone(&Utc),
2424 max_valid_for_secs: 86400,
2425 };
2426
2427 let json = serde_json::to_string(&original).unwrap();
2428 let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
2429
2430 assert_eq!(original.identity_did, deserialized.identity_did);
2431 assert_eq!(original.public_key_hex, deserialized.public_key_hex);
2432 assert_eq!(
2433 original.attestation_chain.len(),
2434 deserialized.attestation_chain.len()
2435 );
2436 }
2437}
2438
2439#[cfg(test)]
2440mod decode_public_key_tests {
2441 use super::*;
2442
2443 #[test]
2444 fn hex_ed25519_32_bytes() {
2445 let hex = "00".repeat(32);
2446 let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::Ed25519).unwrap();
2447 assert_eq!(pk.curve(), auths_crypto::CurveType::Ed25519);
2448 assert_eq!(pk.len(), 32);
2449 }
2450
2451 #[test]
2452 fn hex_p256_33_bytes_compressed() {
2453 let mut bytes = [0u8; 33];
2455 bytes[0] = 0x02;
2456 let hex = hex::encode(bytes);
2457 let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::P256).unwrap();
2458 assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2459 assert_eq!(pk.len(), 33);
2460 }
2461
2462 #[test]
2463 fn bytes_p256_65_uncompressed() {
2464 let mut bytes = [0u8; 65];
2465 bytes[0] = 0x04;
2466 let pk = decode_public_key_bytes(&bytes, auths_crypto::CurveType::P256).unwrap();
2467 assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2468 assert_eq!(pk.len(), 65);
2469 }
2470
2471 #[test]
2472 fn rejects_validation_error() {
2473 let err = decode_public_key_bytes(&[0u8; 50], auths_crypto::CurveType::P256).unwrap_err();
2474 assert!(matches!(err, PublicKeyDecodeError::Validation(_)));
2475 }
2476
2477 #[test]
2478 fn rejects_malformed_hex() {
2479 let err = decode_public_key_hex("zz", auths_crypto::CurveType::P256).unwrap_err();
2480 assert!(matches!(err, PublicKeyDecodeError::InvalidHex(_)));
2481 }
2482}