use crate::error::AttestationError;
use crate::types::{CanonicalDid, IdentityDID};
use chrono::{DateTime, Utc};
use hex;
use json_canon;
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
pub const MAX_ATTESTATION_JSON_SIZE: usize = 64 * 1024;
pub const MAX_JSON_BATCH_SIZE: usize = 1024 * 1024;
pub const MAX_PUBLIC_KEY_HEX_LEN: usize = 64;
pub const MAX_SIGNATURE_HEX_LEN: usize = 128;
pub const MAX_FILE_HASH_HEX_LEN: usize = 64;
const SIGN_COMMIT: &str = "sign_commit";
const SIGN_RELEASE: &str = "sign_release";
const MANAGE_MEMBERS: &str = "manage_members";
const ROTATE_KEYS: &str = "rotate_keys";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ResourceId(String);
impl ResourceId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for ResourceId {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ResourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for ResourceId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ResourceId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl PartialEq<str> for ResourceId {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for ResourceId {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for ResourceId {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum Role {
Admin,
Member,
Readonly,
}
impl Role {
pub fn as_str(&self) -> &str {
match self {
Role::Admin => "admin",
Role::Member => "member",
Role::Readonly => "readonly",
}
}
pub fn default_capabilities(&self) -> Vec<Capability> {
match self {
Role::Admin => vec![
Capability::sign_commit(),
Capability::sign_release(),
Capability::manage_members(),
Capability::rotate_keys(),
],
Role::Member => vec![Capability::sign_commit(), Capability::sign_release()],
Role::Readonly => vec![],
}
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Role {
type Err = RoleParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"admin" => Ok(Role::Admin),
"member" => Ok(Role::Member),
"readonly" => Ok(Role::Readonly),
other => Err(RoleParseError(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
pub struct RoleParseError(String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Ed25519PublicKey([u8; 32]);
impl Ed25519PublicKey {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn try_from_slice(slice: &[u8]) -> Result<Self, Ed25519KeyError> {
let arr: [u8; 32] = slice
.try_into()
.map_err(|_| Ed25519KeyError::InvalidLength(slice.len()))?;
Ok(Self(arr))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn is_zero(&self) -> bool {
self.0 == [0u8; 32]
}
}
impl Serialize for Ed25519PublicKey {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&hex::encode(self.0))
}
}
impl<'de> Deserialize<'de> for Ed25519PublicKey {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
let bytes =
hex::decode(&s).map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len()))
})?;
Ok(Self(arr))
}
}
impl fmt::Display for Ed25519PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::encode(self.0))
}
}
impl AsRef<[u8]> for Ed25519PublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for Ed25519PublicKey {
fn schema_name() -> String {
"Ed25519PublicKey".to_owned()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
format: Some("hex".to_owned()),
metadata: Some(Box::new(schemars::schema::Metadata {
description: Some("Ed25519 public key (32 bytes, hex-encoded)".to_owned()),
..Default::default()
})),
..Default::default()
}
.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Ed25519KeyError {
#[error("expected 32 bytes, got {0}")]
InvalidLength(usize),
#[error("invalid hex: {0}")]
InvalidHex(String),
}
pub const ATTESTATION_VERSION: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypedSignature([u8; 64]);
pub type Ed25519Signature = TypedSignature;
impl TypedSignature {
pub fn from_bytes(bytes: [u8; 64]) -> Self {
Self(bytes)
}
pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
let arr: [u8; 64] = slice
.try_into()
.map_err(|_| SignatureLengthError(slice.len()))?;
Ok(Self(arr))
}
pub fn empty() -> Self {
Self([0u8; 64])
}
pub fn is_empty(&self) -> bool {
self.0 == [0u8; 64]
}
pub fn as_bytes(&self) -> &[u8; 64] {
&self.0
}
}
impl Default for TypedSignature {
fn default() -> Self {
Self::empty()
}
}
impl std::fmt::Display for TypedSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl AsRef<[u8]> for TypedSignature {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for TypedSignature {
fn schema_name() -> String {
"TypedSignature".to_owned()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
format: Some("hex".to_owned()),
metadata: Some(Box::new(schemars::schema::Metadata {
description: Some(
"Curve-agnostic 64-byte signature (Ed25519 or P-256 r||s, hex-encoded). \
Curve is determined by the companion DevicePublicKey."
.to_owned(),
),
..Default::default()
})),
..Default::default()
}
.into()
}
}
impl serde::Serialize for TypedSignature {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&hex::encode(self.0))
}
}
impl<'de> serde::Deserialize<'de> for TypedSignature {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s.is_empty() {
return Ok(Self::empty());
}
let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("expected 64 bytes, got {0}")]
pub struct SignatureLengthError(pub usize);
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DevicePublicKey {
#[cfg_attr(feature = "schema", schemars(skip))]
curve: auths_crypto::CurveType,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
bytes: Vec<u8>,
}
impl DevicePublicKey {
fn canonical_sec1(&self) -> std::borrow::Cow<'_, [u8]> {
if self.curve == auths_crypto::CurveType::P256
&& self.bytes.len() == 65
&& self.bytes[0] == 0x04
{
let mut compressed = vec![0u8; 33];
compressed[0] = if self.bytes[64] & 1 == 0 { 0x02 } else { 0x03 };
compressed[1..].copy_from_slice(&self.bytes[1..33]);
std::borrow::Cow::Owned(compressed)
} else {
std::borrow::Cow::Borrowed(&self.bytes)
}
}
}
impl PartialEq for DevicePublicKey {
fn eq(&self, other: &Self) -> bool {
self.curve == other.curve && self.canonical_sec1() == other.canonical_sec1()
}
}
impl Eq for DevicePublicKey {}
impl std::hash::Hash for DevicePublicKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.curve.hash(state);
self.canonical_sec1().hash(state);
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InvalidKeyError {
#[error("invalid key length for {curve}: expected {expected}, got {actual}")]
InvalidLength {
curve: auths_crypto::CurveType,
expected: &'static str,
actual: usize,
},
}
impl DevicePublicKey {
pub fn try_new(curve: auths_crypto::CurveType, bytes: &[u8]) -> Result<Self, InvalidKeyError> {
let valid = match curve {
auths_crypto::CurveType::Ed25519 => bytes.len() == 32,
auths_crypto::CurveType::P256 => bytes.len() == 33 || bytes.len() == 65,
};
if !valid {
return Err(InvalidKeyError::InvalidLength {
curve,
expected: match curve {
auths_crypto::CurveType::Ed25519 => "32",
auths_crypto::CurveType::P256 => "33 or 65",
},
actual: bytes.len(),
});
}
Ok(Self {
curve,
bytes: bytes.to_vec(),
})
}
pub fn ed25519(bytes: &[u8; 32]) -> Self {
Self {
curve: auths_crypto::CurveType::Ed25519,
bytes: bytes.to_vec(),
}
}
pub fn p256(bytes: &[u8]) -> Result<Self, InvalidKeyError> {
Self::try_new(auths_crypto::CurveType::P256, bytes)
}
pub fn curve(&self) -> auths_crypto::CurveType {
self.curve
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn is_zero(&self) -> bool {
self.bytes.iter().all(|&b| b == 0)
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub async fn verify(
&self,
message: &[u8],
signature: &[u8],
provider: &dyn auths_crypto::CryptoProvider,
) -> Result<(), SignatureVerifyError> {
let result = match self.curve {
auths_crypto::CurveType::Ed25519 => {
provider
.verify_ed25519(&self.bytes, message, signature)
.await
}
auths_crypto::CurveType::P256 => {
provider.verify_p256(&self.bytes, message, signature).await
}
};
result.map_err(|e| SignatureVerifyError::VerificationFailed(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PublicKeyDecodeError {
#[error("invalid hex: {0}")]
InvalidHex(String),
#[error("invalid public key length {len} — expected 32 (Ed25519) or 33/65 (P-256)")]
InvalidLength {
len: usize,
},
#[error("DevicePublicKey validation failed: {0}")]
Validation(String),
}
pub fn decode_public_key_hex(
hex_str: &str,
curve: auths_crypto::CurveType,
) -> Result<DevicePublicKey, PublicKeyDecodeError> {
let bytes =
hex::decode(hex_str.trim()).map_err(|e| PublicKeyDecodeError::InvalidHex(e.to_string()))?;
decode_public_key_bytes(&bytes, curve)
}
pub fn decode_public_key_bytes(
bytes: &[u8],
curve: auths_crypto::CurveType,
) -> Result<DevicePublicKey, PublicKeyDecodeError> {
DevicePublicKey::try_new(curve, bytes)
.map_err(|e| PublicKeyDecodeError::Validation(e.to_string()))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SignatureVerifyError {
#[error("signature verification failed: {0}")]
VerificationFailed(String),
#[error("{0}")]
UnsupportedOnTarget(String),
}
impl From<Ed25519PublicKey> for DevicePublicKey {
fn from(pk: Ed25519PublicKey) -> Self {
Self {
curve: auths_crypto::CurveType::Ed25519,
bytes: pk.as_bytes().to_vec(),
}
}
}
impl Serialize for DevicePublicKey {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = s.serialize_struct("DevicePublicKey", 2)?;
st.serialize_field("curve", &self.curve.to_string())?;
st.serialize_field("key", &hex::encode(&self.bytes))?;
st.end()
}
}
impl<'de> Deserialize<'de> for DevicePublicKey {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(d)?;
if let Some(s) = value.as_str() {
if s.is_empty() {
return Ok(Self::default());
}
let bytes = hex::decode(s)
.map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
let curve = match bytes.len() {
32 => auths_crypto::CurveType::Ed25519,
33 | 65 => auths_crypto::CurveType::P256,
n => {
return Err(serde::de::Error::custom(format!(
"invalid device public key length: {n}"
)));
}
};
return Self::try_new(curve, &bytes)
.map_err(|e| serde::de::Error::custom(e.to_string()));
}
let curve_str = value["curve"]
.as_str()
.ok_or_else(|| serde::de::Error::custom("missing 'curve' field"))?;
let key_hex = value["key"]
.as_str()
.ok_or_else(|| serde::de::Error::custom("missing 'key' field"))?;
let curve = match curve_str {
"ed25519" => auths_crypto::CurveType::Ed25519,
"p256" => auths_crypto::CurveType::P256,
other => {
return Err(serde::de::Error::custom(format!("unknown curve: {other}")));
}
};
if key_hex.is_empty() {
return Err(serde::de::Error::custom("empty key"));
}
let bytes = hex::decode(key_hex)
.map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
Self::try_new(curve, &bytes).map_err(|e| serde::de::Error::custom(e.to_string()))
}
}
impl Default for DevicePublicKey {
fn default() -> Self {
Self {
curve: auths_crypto::CurveType::Ed25519,
bytes: vec![0u8; 32],
}
}
}
impl fmt::Display for DevicePublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.curve, hex::encode(&self.bytes))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignatureAlgorithm {
#[default]
Ed25519,
EcdsaP256,
}
impl fmt::Display for SignatureAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ed25519 => f.write_str("ed25519"),
Self::EcdsaP256 => f.write_str("ecdsa_p256"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcdsaP256PublicKey(Vec<u8>);
impl EcdsaP256PublicKey {
pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
const P256_OID: &[u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
if der.len() < 26 {
return Err(EcdsaP256Error::InvalidKey(
"DER too short for P-256 PKIX key".into(),
));
}
if !der.windows(P256_OID.len()).any(|w| w == P256_OID) {
return Err(EcdsaP256Error::InvalidKey(
"missing P-256 OID in PKIX key".into(),
));
}
Ok(Self(der.to_vec()))
}
pub fn as_der(&self) -> &[u8] {
&self.0
}
pub fn as_sec1_uncompressed(&self) -> Option<&[u8]> {
let start = self.0.len().checked_sub(65)?;
let point = &self.0[start..];
(point[0] == 0x04).then_some(point)
}
}
impl Serialize for EcdsaP256PublicKey {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use base64::Engine;
s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
}
}
impl<'de> Deserialize<'de> for EcdsaP256PublicKey {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use base64::Engine;
let s = String::deserialize(d)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&s)
.map_err(|e| serde::de::Error::custom(format!("invalid base64: {e}")))?;
Self::from_der(&bytes).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcdsaP256Signature(Vec<u8>);
impl EcdsaP256Signature {
pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
if der.is_empty() || der[0] != 0x30 {
return Err(EcdsaP256Error::InvalidSignature(
"not an ASN.1 SEQUENCE".into(),
));
}
Ok(Self(der.to_vec()))
}
pub fn as_der(&self) -> &[u8] {
&self.0
}
}
impl Serialize for EcdsaP256Signature {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use base64::Engine;
s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
}
}
impl<'de> Deserialize<'de> for EcdsaP256Signature {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use base64::Engine;
let s = String::deserialize(d)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&s)
.map_err(|e| serde::de::Error::custom(format!("invalid base64: {e}")))?;
Self::from_der(&bytes).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum EcdsaP256Error {
#[error("invalid ECDSA P-256 key: {0}")]
InvalidKey(String),
#[error("invalid ECDSA P-256 signature: {0}")]
InvalidSignature(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CapabilityError {
#[error("capability is empty")]
Empty,
#[error("capability exceeds 64 chars: {0}")]
TooLong(usize),
#[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
InvalidChars(String),
#[error(
"reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
)]
ReservedNamespace,
#[error("the '{0}' prefix is reserved for infrastructure capabilities")]
ReservedInfraNamespace(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(try_from = "String", into = "String")]
pub struct Capability(String);
impl Capability {
pub const MAX_LEN: usize = 64;
const RESERVED_PREFIX: &'static str = "auths:";
const RESERVED_INFRA_PREFIXES: &'static [&'static str] =
&["compute:", "network:", "storage:", "runtime:", "env:"];
#[inline]
pub fn sign_commit() -> Self {
Self(SIGN_COMMIT.to_string())
}
#[inline]
pub fn sign_release() -> Self {
Self(SIGN_RELEASE.to_string())
}
#[inline]
pub fn manage_members() -> Self {
Self(MANAGE_MEMBERS.to_string())
}
#[inline]
pub fn rotate_keys() -> Self {
Self(ROTATE_KEYS.to_string())
}
pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
let canonical = raw.trim().to_lowercase();
if canonical.is_empty() {
return Err(CapabilityError::Empty);
}
if canonical.len() > Self::MAX_LEN {
return Err(CapabilityError::TooLong(canonical.len()));
}
if !canonical
.chars()
.all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
{
return Err(CapabilityError::InvalidChars(canonical));
}
if canonical.starts_with(Self::RESERVED_PREFIX) {
return Err(CapabilityError::ReservedNamespace);
}
for prefix in Self::RESERVED_INFRA_PREFIXES {
if canonical.starts_with(prefix) {
return Err(CapabilityError::ReservedInfraNamespace(prefix.to_string()));
}
}
Ok(Self(canonical))
}
#[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
pub fn custom(s: impl Into<String>) -> Option<Self> {
Self::parse(&s.into()).ok()
}
#[deprecated(since = "0.2.0", note = "Use parse() for validation")]
pub fn validate_custom(s: &str) -> bool {
Self::parse(s).is_ok()
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_well_known(&self) -> bool {
matches!(
self.0.as_str(),
SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
)
}
pub fn namespace(&self) -> Option<&str> {
self.0.split(':').next().filter(|_| self.0.contains(':'))
}
}
impl fmt::Display for Capability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for Capability {
type Error = CapabilityError;
fn try_from(s: String) -> Result<Self, Self::Error> {
let canonical = s.trim().to_lowercase();
if canonical.is_empty() {
return Err(CapabilityError::Empty);
}
if canonical.len() > Self::MAX_LEN {
return Err(CapabilityError::TooLong(canonical.len()));
}
if !canonical
.chars()
.all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
{
return Err(CapabilityError::InvalidChars(canonical));
}
Ok(Self(canonical))
}
}
impl std::str::FromStr for Capability {
type Err = CapabilityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized = s.trim().to_lowercase().replace('-', "_");
match normalized.as_str() {
"sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
"sign_release" | "signrelease" => Ok(Capability::sign_release()),
"manage_members" | "managemembers" => Ok(Capability::manage_members()),
"rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
_ => Capability::parse(&normalized),
}
}
}
impl From<Capability> for String {
fn from(cap: Capability) -> Self {
cap.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct IdentityBundle {
pub identity_did: IdentityDID,
pub public_key_hex: PublicKeyHex,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub curve: auths_crypto::CurveType,
pub attestation_chain: Vec<Attestation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub kel: Vec<serde_json::Value>,
pub bundle_timestamp: DateTime<Utc>,
pub max_valid_for_secs: u64,
}
impl IdentityBundle {
pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
if age > self.max_valid_for_secs {
return Err(AttestationError::BundleExpired {
age_secs: age,
max_secs: self.max_valid_for_secs,
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Attestation {
pub version: u32,
pub rid: ResourceId,
pub issuer: CanonicalDid,
pub subject: CanonicalDid,
pub device_public_key: DevicePublicKey,
#[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
pub identity_signature: Ed25519Signature,
pub device_signature: Ed25519Signature,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revoked_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
pub timestamp: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_sha: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oidc_binding: Option<OidcBinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegated_by: Option<CanonicalDid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_type: Option<SignerType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment_claim: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct OidcBinding {
pub issuer: String,
pub subject: String,
pub audience: String,
pub token_exp: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum SignerType {
Human,
Agent,
Workload,
}
#[derive(Debug, Clone, Serialize)]
pub struct VerifiedAttestation(Attestation);
impl VerifiedAttestation {
pub fn inner(&self) -> &Attestation {
&self.0
}
pub fn into_inner(self) -> Attestation {
self.0
}
#[doc(hidden)]
pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
Self(attestation)
}
pub(crate) fn from_verified(attestation: Attestation) -> Self {
Self(attestation)
}
}
impl std::ops::Deref for VerifiedAttestation {
type Target = Attestation;
fn deref(&self) -> &Attestation {
&self.0
}
}
#[derive(Serialize, Debug)]
pub struct CanonicalAttestationData<'a> {
pub version: u32,
pub rid: &'a str,
pub issuer: &'a CanonicalDid,
pub subject: &'a CanonicalDid,
#[serde(with = "hex::serde")]
pub device_public_key: &'a [u8],
pub payload: &'a Option<Value>,
pub timestamp: &'a Option<DateTime<Utc>>,
pub expires_at: &'a Option<DateTime<Utc>>,
pub revoked_at: &'a Option<DateTime<Utc>>,
pub note: &'a Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delegated_by: Option<&'a CanonicalDid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_type: Option<&'a SignerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_sha: Option<&'a str>,
}
pub fn canonicalize_attestation_data(
data: &CanonicalAttestationData,
) -> Result<Vec<u8>, AttestationError> {
let canonical_json_string = json_canon::to_string(data).map_err(|e| {
AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
})?;
debug!(
"Generated canonical data (standard): {}",
canonical_json_string
);
Ok(canonical_json_string.into_bytes())
}
impl Attestation {
pub fn is_revoked(&self) -> bool {
self.revoked_at.is_some()
}
pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
return Err(AttestationError::InputTooLarge(format!(
"attestation JSON is {} bytes, max {}",
json_bytes.len(),
MAX_ATTESTATION_JSON_SIZE
)));
}
serde_json::from_slice(json_bytes)
.map_err(|e| AttestationError::SerializationError(e.to_string()))
}
pub fn canonical_data(&self) -> CanonicalAttestationData<'_> {
CanonicalAttestationData {
version: self.version,
rid: &self.rid,
issuer: &self.issuer,
subject: &self.subject,
device_public_key: self.device_public_key.as_bytes(),
payload: &self.payload,
timestamp: &self.timestamp,
expires_at: &self.expires_at,
revoked_at: &self.revoked_at,
note: &self.note,
delegated_by: self.delegated_by.as_ref(),
signer_type: self.signer_type.as_ref(),
commit_sha: self.commit_sha.as_deref(),
}
}
pub fn to_debug_string(&self) -> String {
format!(
"RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
self.rid,
self.issuer,
self.subject, hex::encode(self.device_public_key.as_bytes()),
hex::encode(self.identity_signature.as_bytes()),
hex::encode(self.device_signature.as_bytes()),
self.revoked_at,
self.expires_at,
self.note
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ThresholdPolicy {
pub threshold: u8,
pub signers: Vec<String>,
pub policy_id: PolicyId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<Capability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ceremony_endpoint: Option<String>,
}
impl ThresholdPolicy {
pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
Self {
threshold,
signers,
policy_id: policy_id.into(),
scope: None,
ceremony_endpoint: None,
}
}
pub fn is_valid(&self) -> bool {
if self.threshold < 1 {
return false;
}
if self.threshold as usize > self.signers.len() {
return false;
}
if self.signers.is_empty() {
return false;
}
if self.policy_id.is_empty() {
return false;
}
true
}
pub fn m_of_n(&self) -> (u8, usize) {
(self.threshold, self.signers.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CommitOidError {
#[error("commit OID is empty")]
Empty,
#[error("expected 40 or 64 hex chars, got {0}")]
InvalidLength(usize),
#[error("invalid hex character in commit OID")]
InvalidHex,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[repr(transparent)]
#[serde(try_from = "String")]
pub struct CommitOid(String);
impl CommitOid {
pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
let s = raw.trim().to_lowercase();
if s.is_empty() {
return Err(CommitOidError::Empty);
}
if s.len() != 40 && s.len() != 64 {
return Err(CommitOidError::InvalidLength(s.len()));
}
if !s.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(CommitOidError::InvalidHex);
}
Ok(Self(s))
}
pub fn new_unchecked(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for CommitOid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for CommitOid {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for CommitOid {
type Error = CommitOidError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::parse(&s)
}
}
impl TryFrom<&str> for CommitOid {
type Error = CommitOidError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl FromStr for CommitOid {
type Err = CommitOidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl From<CommitOid> for String {
fn from(oid: CommitOid) -> Self {
oid.0
}
}
impl<'de> Deserialize<'de> for CommitOid {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PublicKeyHexError {
#[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
InvalidLength(usize),
#[error("invalid hex: {0}")]
InvalidHex(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[repr(transparent)]
#[serde(try_from = "String")]
pub struct PublicKeyHex(String);
impl PublicKeyHex {
pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
let s = raw.trim().to_lowercase();
let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
if bytes.len() != 32 && bytes.len() != 33 {
return Err(PublicKeyHexError::InvalidLength(s.len()));
}
Ok(Self(s))
}
pub fn new_unchecked(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for PublicKeyHex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for PublicKeyHex {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for PublicKeyHex {
type Error = PublicKeyHexError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::parse(&s)
}
}
impl TryFrom<&str> for PublicKeyHex {
type Error = PublicKeyHexError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl FromStr for PublicKeyHex {
type Err = PublicKeyHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl From<PublicKeyHex> for String {
fn from(pk: PublicKeyHex) -> Self {
pk.0
}
}
impl<'de> Deserialize<'de> for PublicKeyHex {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct PolicyId(String);
impl PolicyId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for PolicyId {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl fmt::Display for PolicyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for PolicyId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for PolicyId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl PartialEq<str> for PolicyId {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for PolicyId {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use crate::AttestationBuilder;
#[test]
fn capability_serializes_to_snake_case() {
assert_eq!(
serde_json::to_string(&Capability::sign_commit()).unwrap(),
r#""sign_commit""#
);
assert_eq!(
serde_json::to_string(&Capability::sign_release()).unwrap(),
r#""sign_release""#
);
assert_eq!(
serde_json::to_string(&Capability::manage_members()).unwrap(),
r#""manage_members""#
);
assert_eq!(
serde_json::to_string(&Capability::rotate_keys()).unwrap(),
r#""rotate_keys""#
);
}
#[test]
fn capability_deserializes_from_snake_case() {
assert_eq!(
serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
Capability::sign_commit()
);
assert_eq!(
serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
Capability::sign_release()
);
assert_eq!(
serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
Capability::manage_members()
);
assert_eq!(
serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
Capability::rotate_keys()
);
}
#[test]
fn capability_custom_serializes_as_string() {
let cap = Capability::parse("acme:deploy").unwrap();
assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
}
#[test]
fn capability_custom_deserializes_unknown_strings() {
let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
assert_eq!(cap, Capability::parse("custom-capability").unwrap());
}
#[test]
fn capability_parse_accepts_valid_strings() {
assert!(Capability::parse("deploy").is_ok());
assert!(Capability::parse("acme:deploy").is_ok());
assert!(Capability::parse("my-custom-cap").is_ok());
assert!(Capability::parse("org:team:action").is_ok());
assert!(Capability::parse("with_underscore").is_ok()); }
#[test]
fn capability_parse_rejects_invalid_strings() {
assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
assert!(matches!(
Capability::parse(&"a".repeat(65)),
Err(CapabilityError::TooLong(65))
));
assert!(matches!(
Capability::parse("has spaces"),
Err(CapabilityError::InvalidChars(_))
));
assert!(matches!(
Capability::parse("has.dot"),
Err(CapabilityError::InvalidChars(_))
));
}
#[test]
fn capability_parse_rejects_reserved_namespace() {
assert!(matches!(
Capability::parse("auths:custom"),
Err(CapabilityError::ReservedNamespace)
));
assert!(matches!(
Capability::parse("auths:sign_commit"),
Err(CapabilityError::ReservedNamespace)
));
}
#[test]
fn capability_parse_normalizes_to_lowercase() {
let cap = Capability::parse("DEPLOY").unwrap();
assert_eq!(cap.as_str(), "deploy");
let cap = Capability::parse("ACME:Deploy").unwrap();
assert_eq!(cap.as_str(), "acme:deploy");
}
#[test]
fn capability_parse_trims_whitespace() {
let cap = Capability::parse(" deploy ").unwrap();
assert_eq!(cap.as_str(), "deploy");
}
#[test]
fn capability_is_hashable() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Capability::sign_commit());
set.insert(Capability::sign_release());
set.insert(Capability::parse("test").unwrap());
assert_eq!(set.len(), 3);
assert!(set.contains(&Capability::sign_commit()));
}
#[test]
fn capability_equality_with_different_construction_paths() {
let from_constructor = Capability::sign_commit();
let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
assert_eq!(from_constructor, from_deser);
let from_parse = Capability::parse("acme:deploy").unwrap();
let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
assert_eq!(from_parse, from_deser);
}
#[test]
fn capability_display_matches_canonical_form() {
assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
assert_eq!(Capability::sign_release().to_string(), "sign_release");
assert_eq!(Capability::manage_members().to_string(), "manage_members");
assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
assert_eq!(
Capability::parse("acme:deploy").unwrap().to_string(),
"acme:deploy"
);
}
#[test]
fn capability_as_str_returns_canonical_form() {
assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
assert_eq!(Capability::sign_release().as_str(), "sign_release");
assert_eq!(Capability::manage_members().as_str(), "manage_members");
assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
assert_eq!(
Capability::parse("acme:deploy").unwrap().as_str(),
"acme:deploy"
);
}
#[test]
fn capability_is_well_known() {
assert!(Capability::sign_commit().is_well_known());
assert!(Capability::sign_release().is_well_known());
assert!(Capability::manage_members().is_well_known());
assert!(Capability::rotate_keys().is_well_known());
assert!(!Capability::parse("custom").unwrap().is_well_known());
}
#[test]
fn capability_namespace() {
assert_eq!(
Capability::parse("acme:deploy").unwrap().namespace(),
Some("acme")
);
assert_eq!(
Capability::parse("org:team:action").unwrap().namespace(),
Some("org")
);
assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
}
#[test]
fn capability_vec_serializes_as_array() {
let caps = vec![Capability::sign_commit(), Capability::sign_release()];
let json = serde_json::to_string(&caps).unwrap();
assert_eq!(json, r#"["sign_commit","sign_release"]"#);
}
#[test]
fn capability_vec_deserializes_from_array() {
let json = r#"["sign_commit","manage_members","custom-cap"]"#;
let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
assert_eq!(caps.len(), 3);
assert_eq!(caps[0], Capability::sign_commit());
assert_eq!(caps[1], Capability::manage_members());
assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
}
#[test]
fn capability_serde_roundtrip_well_known() {
let caps = vec![
Capability::sign_commit(),
Capability::sign_release(),
Capability::manage_members(),
Capability::rotate_keys(),
];
for cap in caps {
let json = serde_json::to_string(&cap).unwrap();
let roundtrip: Capability = serde_json::from_str(&json).unwrap();
assert_eq!(cap, roundtrip);
}
}
#[test]
fn capability_serde_roundtrip_custom() {
let caps = vec![
Capability::parse("deploy").unwrap(),
Capability::parse("acme:deploy").unwrap(),
Capability::parse("org:team:action").unwrap(),
];
for cap in caps {
let json = serde_json::to_string(&cap).unwrap();
let roundtrip: Capability = serde_json::from_str(&json).unwrap();
assert_eq!(cap, roundtrip);
}
}
#[test]
fn attestation_old_json_without_delegated_by_deserializes() {
let old_json = r#"{
"version": 1,
"rid": "test-rid",
"issuer": "did:keri:Eissuer",
"subject": "did:key:zSubject",
"device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
"identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"revoked_at": null,
"timestamp": null
}"#;
let att: Attestation = serde_json::from_str(old_json).unwrap();
assert_eq!(att.delegated_by, None);
}
#[test]
fn attestation_delegated_by_serializes_correctly() {
let att = AttestationBuilder::default()
.rid("test-rid")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
.build();
let json = serde_json::to_string(&att).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
}
#[test]
fn attestation_omits_delegated_by_when_absent() {
let att = AttestationBuilder::default()
.rid("test-rid")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.build();
let json = serde_json::to_string(&att).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("delegated_by").is_none());
}
#[test]
fn attestation_delegated_by_roundtrips() {
let original = AttestationBuilder::default()
.rid("test-rid")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
.build();
let json = serde_json::to_string(&original).unwrap();
let deserialized: Attestation = serde_json::from_str(&json).unwrap();
assert_eq!(original.delegated_by, deserialized.delegated_by);
}
#[test]
fn threshold_policy_new_creates_valid_policy() {
let policy = ThresholdPolicy::new(
2,
vec![
"did:key:alice".to_string(),
"did:key:bob".to_string(),
"did:key:carol".to_string(),
],
"test-policy".to_string(),
);
assert_eq!(policy.threshold, 2);
assert_eq!(policy.signers.len(), 3);
assert_eq!(policy.policy_id, "test-policy");
assert!(policy.scope.is_none());
assert!(policy.ceremony_endpoint.is_none());
}
#[test]
fn threshold_policy_is_valid_checks_constraints() {
let valid = ThresholdPolicy::new(
2,
vec!["a".to_string(), "b".to_string(), "c".to_string()],
"policy".to_string(),
);
assert!(valid.is_valid());
let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
assert!(!zero_threshold.is_valid());
let too_high = ThresholdPolicy::new(
3,
vec!["a".to_string(), "b".to_string()],
"policy".to_string(),
);
assert!(!too_high.is_valid());
let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
assert!(!no_signers.is_valid());
let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
assert!(!no_id.is_valid());
}
#[test]
fn threshold_policy_m_of_n_returns_correct_values() {
let policy = ThresholdPolicy::new(
2,
vec!["a".to_string(), "b".to_string(), "c".to_string()],
"policy".to_string(),
);
let (m, n) = policy.m_of_n();
assert_eq!(m, 2);
assert_eq!(n, 3);
}
#[test]
fn threshold_policy_serializes_correctly() {
let mut policy = ThresholdPolicy::new(
2,
vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
"release-policy".to_string(),
);
policy.scope = Some(Capability::sign_release());
policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
let json = serde_json::to_string(&policy).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["threshold"], 2);
assert_eq!(parsed["signers"][0], "did:key:alice");
assert_eq!(parsed["policy_id"], "release-policy");
assert_eq!(parsed["scope"], "sign_release");
assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
}
#[test]
fn threshold_policy_without_optional_fields_omits_them() {
let policy =
ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
let json = serde_json::to_string(&policy).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("scope").is_none());
assert!(parsed.get("ceremony_endpoint").is_none());
}
#[test]
fn threshold_policy_roundtrips() {
let mut original = ThresholdPolicy::new(
3,
vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
],
"important-policy".to_string(),
);
original.scope = Some(Capability::rotate_keys());
let json = serde_json::to_string(&original).unwrap();
let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn identity_bundle_serializes_correctly() {
let bundle = IdentityBundle {
identity_did: IdentityDID::new_unchecked("did:keri:test123"),
public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
curve: Default::default(),
attestation_chain: vec![],
kel: vec![],
bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
max_valid_for_secs: 86400,
};
let json = serde_json::to_string(&bundle).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["identity_did"], "did:keri:test123");
assert_eq!(parsed["public_key_hex"], "aabbccdd");
assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
}
#[test]
fn identity_bundle_deserializes_correctly() {
let json = r#"{
"identity_did": "did:keri:abc123",
"public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
"attestation_chain": [],
"bundle_timestamp": "2099-01-01T00:00:00Z",
"max_valid_for_secs": 86400
}"#;
let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
assert_eq!(
bundle.public_key_hex.as_str(),
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
);
assert!(bundle.attestation_chain.is_empty());
}
#[test]
fn identity_bundle_roundtrips() {
let attestation = AttestationBuilder::default()
.rid("test-rid")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.build();
let original = IdentityBundle {
identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
public_key_hex: PublicKeyHex::new_unchecked(
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
),
curve: Default::default(),
attestation_chain: vec![attestation],
kel: vec![],
bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
max_valid_for_secs: 86400,
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
assert_eq!(original.identity_did, deserialized.identity_did);
assert_eq!(original.public_key_hex, deserialized.public_key_hex);
assert_eq!(
original.attestation_chain.len(),
deserialized.attestation_chain.len()
);
}
}
#[cfg(test)]
mod decode_public_key_tests {
use super::*;
#[test]
fn hex_ed25519_32_bytes() {
let hex = "00".repeat(32);
let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::Ed25519).unwrap();
assert_eq!(pk.curve(), auths_crypto::CurveType::Ed25519);
assert_eq!(pk.len(), 32);
}
#[test]
fn hex_p256_33_bytes_compressed() {
let mut bytes = [0u8; 33];
bytes[0] = 0x02;
let hex = hex::encode(bytes);
let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::P256).unwrap();
assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
assert_eq!(pk.len(), 33);
}
#[test]
fn bytes_p256_65_uncompressed() {
let mut bytes = [0u8; 65];
bytes[0] = 0x04;
let pk = decode_public_key_bytes(&bytes, auths_crypto::CurveType::P256).unwrap();
assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
assert_eq!(pk.len(), 65);
}
#[test]
fn rejects_validation_error() {
let err = decode_public_key_bytes(&[0u8; 50], auths_crypto::CurveType::P256).unwrap_err();
assert!(matches!(err, PublicKeyDecodeError::Validation(_)));
}
#[test]
fn rejects_malformed_hex() {
let err = decode_public_key_hex("zz", auths_crypto::CurveType::P256).unwrap_err();
assert!(matches!(err, PublicKeyDecodeError::InvalidHex(_)));
}
}