use ciborium::Value;
use ed25519_dalek::{Signer, SigningKey};
use smallvec::SmallVec;
use std::time::SystemTime;
use thiserror::Error;
use crate::authority::capability::CapabilitySet;
use crate::identity::{KeyId, PublicKey, ServiceIdentity, SignatureAlgorithm};
use crate::ingress::{DerivationReason, MAX_CHAIN_DEPTH};
use crate::proto::Did;
use crate::resolver::DidResolutionError;
use crate::wire::canonical_cbor;
use super::signature::ClaimSignature;
pub(crate) const ATTRIBUTION_RECEIPT_DOMAIN_TAG: &[u8] =
b"kryphocron/v1/attribution-receipt/";
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributionPrincipal {
User(Did),
Service(ServiceIdentity),
}
impl AttributionPrincipal {
#[must_use]
pub fn did(&self) -> &Did {
match self {
AttributionPrincipal::User(d) => d,
AttributionPrincipal::Service(s) => s.service_did(),
}
}
#[must_use]
pub fn key_id(&self) -> Option<KeyId> {
match self {
AttributionPrincipal::User(_) => None,
AttributionPrincipal::Service(s) => Some(s.key_id()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributionChainWire {
pub origin: AttributionPrincipal,
pub entries: SmallVec<[AttributionEntryWire; MAX_CHAIN_DEPTH]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AttributionEntryWire {
pub principal: AttributionPrincipal,
pub derivation_reason: DerivationReason,
pub derived_at: SystemTime,
pub granted_capabilities: CapabilitySet,
pub receipt: DelegationReceipt,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DelegationReceiptPayload {
pub previous_principal_did: Did,
pub previous_key_id: KeyId,
pub recipient_principal_did: Did,
pub recipient_key_id: KeyId,
pub derivation_reason: DerivationReason,
pub granted_capabilities: CapabilitySet,
pub derived_at: SystemTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct DelegationReceipt {
pub algorithm: SignatureAlgorithm,
pub bytes: [u8; 64],
}
impl From<ClaimSignature> for DelegationReceipt {
fn from(sig: ClaimSignature) -> Self {
DelegationReceipt {
algorithm: sig.algorithm,
bytes: sig.bytes,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ReceiptVerificationFailure {
#[error("receipt signature invalid")]
SignatureInvalid,
#[error("previous principal unresolvable: {0}")]
PreviousPrincipalUnresolvable(DidResolutionError),
#[error("algorithm not accepted: {0:?}")]
AlgorithmNotAccepted(SignatureAlgorithm),
#[error("receipt malformed")]
Malformed,
#[error(
"capability expansion at hop {hop}: attempted exceeds available"
)]
CapabilityExpansion {
hop: u8,
attempted: CapabilitySet,
available: CapabilitySet,
},
#[error("key not in rotation history: {previous_key_id:?}")]
KeyNotInRotationHistory {
previous_key_id: KeyId,
},
}
#[must_use]
pub(crate) fn delegation_receipt_payload_canonical_bytes(
payload: &DelegationReceiptPayload,
) -> Vec<u8> {
canonical_cbor::to_canonical_bytes(delegation_receipt_payload_value(payload))
}
fn delegation_receipt_payload_value(p: &DelegationReceiptPayload) -> Value {
Value::Map(vec![
(
Value::Text("previous_principal_did".into()),
Value::Text(p.previous_principal_did.as_str().to_string()),
),
(
Value::Text("previous_key_id".into()),
Value::Bytes(p.previous_key_id.as_bytes().to_vec()),
),
(
Value::Text("recipient_principal_did".into()),
Value::Text(p.recipient_principal_did.as_str().to_string()),
),
(
Value::Text("recipient_key_id".into()),
Value::Bytes(p.recipient_key_id.as_bytes().to_vec()),
),
(
Value::Text("derivation_reason".into()),
derivation_reason_value(&p.derivation_reason),
),
(
Value::Text("granted_capabilities".into()),
capability_set_value(&p.granted_capabilities),
),
(
Value::Text("derived_at".into()),
system_time_value(p.derived_at),
),
])
}
fn derivation_reason_value(r: &DerivationReason) -> Value {
match r {
DerivationReason::DropPrivilegeToAnonymous => Value::Map(vec![(
Value::Text("kind".into()),
Value::Text("drop_privilege_to_anonymous".into()),
)]),
DerivationReason::NarrowCapabilities { dropped } => Value::Map(vec![
(Value::Text("kind".into()), Value::Text("narrow_capabilities".into())),
(Value::Text("dropped".into()), capability_set_value(dropped)),
]),
DerivationReason::ServiceToServiceDelegation { trust_declaration_id } => {
Value::Map(vec![
(
Value::Text("kind".into()),
Value::Text("service_to_service_delegation".into()),
),
(
Value::Text("trust_declaration_id".into()),
Value::Bytes(trust_declaration_id.as_bytes().to_vec()),
),
])
}
}
}
fn capability_set_value(s: &CapabilitySet) -> Value {
Value::Array(
s.kinds()
.iter()
.map(|c| Value::Text(c.wire_name().to_string()))
.collect(),
)
}
fn system_time_value(t: SystemTime) -> Value {
let secs = t
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime before UNIX_EPOCH not supported")
.as_secs();
Value::Integer(secs.into())
}
#[must_use]
pub fn sign_delegation_receipt(
payload: &DelegationReceiptPayload,
signing_key: &SigningKey,
) -> DelegationReceipt {
let canonical = delegation_receipt_payload_canonical_bytes(payload);
let mut signing_input =
Vec::with_capacity(ATTRIBUTION_RECEIPT_DOMAIN_TAG.len() + canonical.len());
signing_input.extend_from_slice(ATTRIBUTION_RECEIPT_DOMAIN_TAG);
signing_input.extend_from_slice(&canonical);
let sig = signing_key.sign(&signing_input);
DelegationReceipt {
algorithm: SignatureAlgorithm::Ed25519,
bytes: sig.to_bytes(),
}
}
pub(crate) fn verify_delegation_receipt(
payload: &DelegationReceiptPayload,
receipt: &DelegationReceipt,
public_key: &PublicKey,
) -> bool {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
if receipt.algorithm != SignatureAlgorithm::Ed25519
|| public_key.algorithm != SignatureAlgorithm::Ed25519
{
return false;
}
let Ok(vk) = VerifyingKey::from_bytes(&public_key.bytes) else {
return false;
};
let canonical = delegation_receipt_payload_canonical_bytes(payload);
let mut signing_input =
Vec::with_capacity(ATTRIBUTION_RECEIPT_DOMAIN_TAG.len() + canonical.len());
signing_input.extend_from_slice(ATTRIBUTION_RECEIPT_DOMAIN_TAG);
signing_input.extend_from_slice(&canonical);
let sig = Signature::from_bytes(&receipt.bytes);
vk.verify(&signing_input, &sig).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receipt_verification_failure_has_six_variants() {
let _v1 = ReceiptVerificationFailure::SignatureInvalid;
let _v2 = ReceiptVerificationFailure::PreviousPrincipalUnresolvable(
DidResolutionError::NotFound,
);
let _v3 = ReceiptVerificationFailure::AlgorithmNotAccepted(
SignatureAlgorithm::Ed25519,
);
let _v4 = ReceiptVerificationFailure::Malformed;
let _v5 = ReceiptVerificationFailure::CapabilityExpansion {
hop: 0,
attempted: CapabilitySet::empty(),
available: CapabilitySet::empty(),
};
let _v6 = ReceiptVerificationFailure::KeyNotInRotationHistory {
previous_key_id: KeyId::from_bytes([0; 32]),
};
}
#[test]
fn attribution_principal_did_accessor() {
let p = AttributionPrincipal::User(Did::new("did:plc:example").unwrap());
assert_eq!(p.did().as_str(), "did:plc:example");
assert!(p.key_id().is_none());
}
#[test]
fn attribution_entry_wire_carries_granted_capabilities_round_5() {
let entry = AttributionEntryWire {
principal: AttributionPrincipal::User(Did::new("did:plc:u").unwrap()),
derivation_reason: crate::ingress::DerivationReason::DropPrivilegeToAnonymous,
derived_at: std::time::SystemTime::UNIX_EPOCH,
granted_capabilities: CapabilitySet::empty(),
receipt: DelegationReceipt {
algorithm: SignatureAlgorithm::Ed25519,
bytes: [0; 64],
},
};
assert!(entry.granted_capabilities.is_empty());
}
#[test]
fn delegation_receipt_payload_canonicalizes_principals_as_did_key_id_round_5() {
let p = DelegationReceiptPayload {
previous_principal_did: Did::new("did:plc:from").unwrap(),
previous_key_id: KeyId::from_bytes([1; 32]),
recipient_principal_did: Did::new("did:plc:to").unwrap(),
recipient_key_id: KeyId::from_bytes([2; 32]),
derivation_reason: crate::ingress::DerivationReason::DropPrivilegeToAnonymous,
granted_capabilities: CapabilitySet::empty(),
derived_at: std::time::SystemTime::UNIX_EPOCH,
};
assert_eq!(p.previous_key_id, KeyId::from_bytes([1; 32]));
assert_eq!(p.recipient_key_id, KeyId::from_bytes([2; 32]));
}
}