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
19pub const ATTESTATION_VERSION: u32 = 1;
21
22const MAX_CREATION_SKEW_SECS: i64 = 5 * 60;
24
25#[derive(Serialize, Debug)] pub 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>>, pub note: &'a Option<String>,
36}
37
38#[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 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 #[allow(clippy::disallowed_methods)]
95 let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str());
97 #[allow(clippy::disallowed_methods)]
98 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 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 let message_to_sign = canonicalize_attestation_data(&data_to_canonicalize)?;
125
126 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 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 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
191pub 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}