use crate::attestation::create::{CanonicalRevocationData, canonicalize_revocation_data};
use auths_core::signing::{PassphraseProvider, SecureSigner};
use auths_core::storage::keychain::{IdentityDID, KeyAlias};
use auths_verifier::core::{Attestation, Ed25519Signature, ResourceId};
use auths_verifier::error::AttestationError;
use auths_verifier::types::CanonicalDid;
use chrono::{DateTime, Utc};
use log::{debug, warn};
use serde_json::Value;
pub const REVOCATION_VERSION: u32 = 1;
pub struct RevocationInput<'a> {
pub rid: &'a str,
pub identity_did: &'a IdentityDID,
pub subject: &'a CanonicalDid,
pub device_public_key: &'a [u8],
pub device_curve: auths_crypto::CurveType,
pub note: Option<String>,
pub payload: Option<Value>,
pub timestamp: DateTime<Utc>,
pub identity_alias: &'a KeyAlias,
}
pub fn create_signed_revocation(
input: RevocationInput<'_>,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
) -> Result<Attestation, AttestationError> {
let RevocationInput {
rid,
identity_did,
subject,
device_public_key,
device_curve,
note,
payload: payload_arg,
timestamp: timestamp_arg,
identity_alias,
} = input;
warn!("Creating revocation for subject {}", subject);
let revoked_at_value = Some(timestamp_arg);
#[allow(clippy::disallowed_methods)]
let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str());
let data_to_canonicalize_revocation = CanonicalRevocationData {
version: REVOCATION_VERSION,
rid,
issuer: &issuer_canonical,
subject,
timestamp: &Some(timestamp_arg),
revoked_at: &revoked_at_value,
note: ¬e,
};
let canonical_bytes = canonicalize_revocation_data(&data_to_canonicalize_revocation)?;
debug!(
"Canonical revocation data: {}",
String::from_utf8_lossy(&canonical_bytes)
);
debug!(
"Signing revocation with identity alias '{}'",
identity_alias
);
let identity_sig_bytes = signer
.sign_with_alias(identity_alias, passphrase_provider, &canonical_bytes)
.map_err(|e| {
AttestationError::SigningError(format!(
"Failed to sign revocation with identity key '{}': {}",
identity_alias, e
))
})?;
let identity_signature = Ed25519Signature::try_from_slice(&identity_sig_bytes)
.map_err(|e| AttestationError::SigningError(e.to_string()))?;
debug!("Revocation signature obtained successfully");
#[allow(clippy::disallowed_methods)]
let revocation_issuer = CanonicalDid::new_unchecked(identity_did.as_str());
Ok(Attestation {
version: REVOCATION_VERSION,
#[allow(clippy::disallowed_methods)]
subject: CanonicalDid::new_unchecked(subject.as_str()),
issuer: revocation_issuer,
rid: ResourceId::new(rid),
payload: payload_arg.clone(),
timestamp: Some(timestamp_arg),
expires_at: None,
revoked_at: Some(timestamp_arg),
note: note.clone(),
device_public_key: auths_verifier::DevicePublicKey::try_new(device_curve, device_public_key)
.map_err(|e| AttestationError::InvalidInput(e.to_string()))?,
identity_signature,
device_signature: Ed25519Signature::empty(),
delegated_by: None,
signer_type: None,
environment_claim: None,
commit_sha: None,
commit_message: None,
author: None,
oidc_binding: None,
})
}
use crate::domain_separation::REVOCATION_PRESIGNED_CONTEXT;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PresignedRevocation {
pub device_did: String,
pub anchor_sn: u128,
pub not_before: DateTime<Utc>,
pub not_after: DateTime<Utc>,
pub issuer: String,
pub signature: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum PresignedRevocationError {
#[error("signing failed: {0}")]
Signing(String),
#[error("window validation failed: not_before {nb} >= not_after {na}")]
InvalidWindow { nb: String, na: String },
#[error("attestation error: {0}")]
Attestation(#[from] AttestationError),
}
pub fn canonicalize_presigned_revocation(
issuer: &str,
device_did: &str,
anchor_sn: u128,
not_before: DateTime<Utc>,
not_after: DateTime<Utc>,
) -> Vec<u8> {
let mut out = Vec::with_capacity(256);
out.extend_from_slice(REVOCATION_PRESIGNED_CONTEXT);
out.push(b'\n');
out.extend_from_slice(issuer.as_bytes());
out.push(b'\n');
out.extend_from_slice(device_did.as_bytes());
out.push(b'\n');
out.extend_from_slice(anchor_sn.to_string().as_bytes());
out.push(b'\n');
out.extend_from_slice(not_before.to_rfc3339().as_bytes());
out.push(b'\n');
out.extend_from_slice(not_after.to_rfc3339().as_bytes());
out
}
#[allow(clippy::too_many_arguments)]
pub fn create_presigned_revocation(
identity_did: &IdentityDID,
device_did: &CanonicalDid,
anchor_sn: u128,
not_before: DateTime<Utc>,
not_after: DateTime<Utc>,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
identity_alias: &KeyAlias,
) -> Result<PresignedRevocation, PresignedRevocationError> {
if not_before >= not_after {
return Err(PresignedRevocationError::InvalidWindow {
nb: not_before.to_rfc3339(),
na: not_after.to_rfc3339(),
});
}
let canonical = canonicalize_presigned_revocation(
identity_did.as_str(),
device_did.as_str(),
anchor_sn,
not_before,
not_after,
);
let sig_bytes = signer
.sign_with_alias(identity_alias, passphrase_provider, &canonical)
.map_err(|e| PresignedRevocationError::Signing(e.to_string()))?;
Ok(PresignedRevocation {
device_did: device_did.as_str().to_string(),
anchor_sn,
not_before,
not_after,
issuer: identity_did.as_str().to_string(),
signature: sig_bytes,
})
}
#[cfg(test)]
mod presigned_tests {
use super::*;
#[test]
fn canonical_bytes_include_context_label() {
let bytes = canonicalize_presigned_revocation(
"did:keri:ETest",
"did:key:z6MkTest",
42,
Utc::now(),
Utc::now() + chrono::Duration::days(1),
);
let s = String::from_utf8_lossy(&bytes);
assert!(s.starts_with("auths-revocation-presigned-v1\n"));
assert!(s.contains("\ndid:keri:ETest\n"));
assert!(s.contains("\ndid:key:z6MkTest\n"));
assert!(s.contains("\n42\n"));
}
#[test]
fn canonical_bytes_differ_from_live_revocation_context() {
use crate::domain_separation::REVOCATION_LIVE_CONTEXT;
assert_ne!(REVOCATION_LIVE_CONTEXT, REVOCATION_PRESIGNED_CONTEXT);
}
#[test]
fn canonical_bytes_include_timestamps_in_rfc3339_form() {
use chrono::TimeZone;
let nb = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let na = Utc.with_ymd_and_hms(2036, 1, 1, 0, 0, 0).unwrap();
let bytes =
canonicalize_presigned_revocation("did:keri:ETest", "did:key:z6MkTest", 7, nb, na);
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("2026-01-01T00:00:00+00:00"));
assert!(s.contains("2036-01-01T00:00:00+00:00"));
}
}