Skip to main content

auths_id/attestation/
create.rs

1use crate::storage::git_refs::AttestationMetadata;
2
3use auths_core::signing::{PassphraseProvider, SecureSigner};
4use auths_core::storage::keychain::{IdentityDID, KeyAlias};
5use auths_verifier::Capability;
6use auths_verifier::core::{
7    Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, Role,
8    canonicalize_attestation_data,
9};
10use auths_verifier::error::AttestationError;
11use auths_verifier::types::{CanonicalDid, DeviceDID};
12
13use chrono::{DateTime, Utc};
14use log::debug;
15use ring::signature::ED25519_PUBLIC_KEY_LEN;
16use serde::Serialize;
17use serde_json::Value;
18
19/// Current attestation version - includes org fields in signed envelope
20pub const ATTESTATION_VERSION: u32 = 1;
21
22/// Maximum allowed clock drift at creation time (seconds)
23const MAX_CREATION_SKEW_SECS: i64 = 5 * 60;
24
25/// NEW: Data structure specifically for canonicalizing revocation statements.
26/// Excludes fields not relevant to the revocation itself (device_pk, payload, expires_at).
27#[derive(Serialize, Debug)] // Added Debug
28pub struct CanonicalRevocationData<'a> {
29    pub version: u32,
30    pub rid: &'a str,
31    pub issuer: &'a CanonicalDid,
32    pub subject: &'a DeviceDID,
33    pub timestamp: &'a Option<DateTime<Utc>>,
34    pub revoked_at: &'a Option<DateTime<Utc>>, // Should always be Some(...)
35    pub note: &'a Option<String>,
36}
37
38/// Creates a signed attestation by signing internally using the provided SecureSigner.
39///
40/// This function constructs the canonical attestation data, signs it using the signer
41/// for both identity and device (if device_alias is provided), and returns the complete
42/// attestation with embedded signatures.
43///
44/// # Arguments
45/// * `rid` - Resource identifier for this attestation
46/// * `identity_did` - The identity DID (e.g., "did:keri:...") issuing the attestation
47/// * `device_did` - The device DID being attested
48/// * `device_public_key` - The 32-byte Ed25519 public key of the device
49/// * `payload` - Optional JSON payload for the attestation
50/// * `meta` - Attestation metadata (timestamp, expiry, notes)
51/// * `signer` - SecureSigner implementation for signing operations
52/// * `passphrase_provider` - Provider for obtaining passphrases during signing
53/// * `identity_alias` - Optional alias of the identity key in the keychain (None = device-only signing)
54/// * `device_alias` - Optional alias of the device key (None means no device signature)
55/// * `capabilities` - Capabilities to grant (included in the signed envelope)
56/// * `role` - Optional org role (e.g., "admin", "member") included in the signed envelope
57/// * `delegated_by` - Optional DID of the delegator included in the signed envelope
58#[allow(clippy::too_many_arguments)]
59pub fn create_signed_attestation(
60    now: DateTime<Utc>,
61    rid: &str,
62    identity_did: &IdentityDID,
63    device_did: &DeviceDID,
64    device_public_key: &[u8],
65    payload: Option<Value>,
66    meta: &AttestationMetadata,
67    signer: &dyn SecureSigner,
68    passphrase_provider: &dyn PassphraseProvider,
69    identity_alias: Option<&KeyAlias>,
70    device_alias: Option<&KeyAlias>,
71    capabilities: Vec<Capability>,
72    role: Option<Role>,
73    delegated_by: Option<IdentityDID>,
74) -> Result<Attestation, AttestationError> {
75    if device_public_key.len() != ED25519_PUBLIC_KEY_LEN {
76        return Err(AttestationError::InvalidInput(format!(
77            "Device public key length must be {}",
78            ED25519_PUBLIC_KEY_LEN
79        )));
80    }
81
82    // Validate timestamp is not too far from current time (clock drift protection)
83    if let Some(ts) = meta.timestamp {
84        let drift = (now - ts).num_seconds().abs();
85        if drift > MAX_CREATION_SKEW_SECS {
86            return Err(AttestationError::InvalidInput(format!(
87                "System clock drift {}s exceeds {}s limit",
88                drift, MAX_CREATION_SKEW_SECS
89            )));
90        }
91    }
92
93    // Construct the canonical data to be signed
94    #[allow(clippy::disallowed_methods)]
95    // INVARIANT: identity_did is an IdentityDID which guarantees valid DID format
96    let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str());
97    #[allow(clippy::disallowed_methods)]
98    // INVARIANT: device_did is a validated DeviceDID from the caller
99    let subject_canonical = CanonicalDid::new_unchecked(device_did.as_str());
100    let delegated_canonical = delegated_by.as_ref().map(|d| CanonicalDid::from(d.clone()));
101    let data_to_canonicalize = CanonicalAttestationData {
102        version: ATTESTATION_VERSION,
103        rid,
104        issuer: &issuer_canonical,
105        subject: &subject_canonical,
106        device_public_key,
107        payload: &payload,
108        timestamp: &meta.timestamp,
109        expires_at: &meta.expires_at,
110        revoked_at: &None,
111        note: &meta.note,
112        // Org fields included in signed envelope
113        role: role.as_ref().map(|r| r.as_str()),
114        capabilities: if capabilities.is_empty() {
115            None
116        } else {
117            Some(&capabilities)
118        },
119        delegated_by: delegated_canonical.as_ref(),
120        signer_type: None,
121    };
122
123    // Canonicalize the attestation data
124    let message_to_sign = canonicalize_attestation_data(&data_to_canonicalize)?;
125
126    // Sign with the identity key (if alias provided)
127    let identity_signature = if let Some(alias) = identity_alias {
128        debug!("Signing attestation with identity alias '{}'", alias);
129        let sig = signer
130            .sign_with_alias(alias, passphrase_provider, &message_to_sign)
131            .map_err(|e| {
132                AttestationError::SigningError(format!(
133                    "Failed to sign with identity key '{}': {}",
134                    alias, e
135                ))
136            })?;
137        debug!("Identity signature obtained successfully");
138        Ed25519Signature::try_from_slice(&sig)
139            .map_err(|e| AttestationError::SigningError(e.to_string()))?
140    } else {
141        debug!("No identity alias provided, skipping identity signature (device-only attestation)");
142        Ed25519Signature::empty()
143    };
144
145    // Sign with the device key if alias provided
146    let device_signature = if let Some(alias) = device_alias {
147        debug!("Signing attestation with device alias '{}'", alias);
148        let sig = signer
149            .sign_with_alias(alias, passphrase_provider, &message_to_sign)
150            .map_err(|e| {
151                AttestationError::SigningError(format!(
152                    "Failed to sign with device key '{}': {}",
153                    alias, e
154                ))
155            })?;
156        debug!("Device signature obtained successfully");
157        Ed25519Signature::try_from_slice(&sig)
158            .map_err(|e| AttestationError::SigningError(e.to_string()))?
159    } else {
160        debug!("No device alias provided, skipping device signature");
161        Ed25519Signature::empty()
162    };
163
164    // Construct final attestation
165    Ok(Attestation {
166        version: ATTESTATION_VERSION,
167        subject: subject_canonical,
168        issuer: issuer_canonical,
169        rid: ResourceId::new(rid),
170        payload: payload.clone(),
171        timestamp: meta.timestamp,
172        expires_at: meta.expires_at,
173        revoked_at: None,
174        note: meta.note.clone(),
175        device_public_key: Ed25519PublicKey::try_from_slice(device_public_key)
176            .map_err(|e| AttestationError::InvalidInput(e.to_string()))?,
177        identity_signature,
178        device_signature,
179        role,
180        capabilities,
181        delegated_by: delegated_canonical,
182        signer_type: None,
183        environment_claim: None,
184        commit_sha: None,
185        commit_message: None,
186        author: None,
187        oidc_binding: None,
188    })
189}
190
191/// Generates the canonical byte representation specifically for revocation data.
192pub fn canonicalize_revocation_data(
193    data: &CanonicalRevocationData,
194) -> Result<Vec<u8>, AttestationError> {
195    let canonical_json_string = json_canon::to_string(data).map_err(|e| {
196        AttestationError::SerializationError(format!(
197            "Failed to create canonical JSON for revocation: {}",
198            e
199        ))
200    })?;
201    debug!(
202        "Generated canonical data (revocation): {}",
203        canonical_json_string
204    );
205    Ok(canonical_json_string.into_bytes())
206}