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;
pub use auths_keri::capability::{
Capability, CapabilityError, MANAGE_MEMBERS, ROTATE_KEYS, SIGN_COMMIT, SIGN_RELEASE,
};
#[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, 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 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(_)));
}
}