1use crate::storage::git_refs::AttestationMetadata;
2
3use auths_core::signing::{PassphraseProvider, SecureSigner};
4use auths_core::storage::keychain::{IdentityDID, KeyAlias};
5use auths_verifier::core::{
6 Attestation, Ed25519Signature, ResourceId, SignerType, canonicalize_attestation_data,
7};
8use auths_verifier::error::AttestationError;
9use auths_verifier::types::CanonicalDid;
10
11use chrono::{DateTime, Utc};
12use log::debug;
13use serde::Serialize;
14use serde_json::Value;
15
16pub const ATTESTATION_VERSION: u32 = 1;
18
19const MAX_CREATION_SKEW_SECS: i64 = 5 * 60;
21
22#[derive(Serialize, Debug)] pub struct CanonicalRevocationData<'a> {
26 pub version: u32,
27 pub rid: &'a str,
28 pub issuer: &'a CanonicalDid,
29 pub subject: &'a CanonicalDid,
30 pub timestamp: &'a Option<DateTime<Utc>>,
31 pub revoked_at: &'a Option<DateTime<Utc>>, pub note: &'a Option<String>,
33}
34
35pub struct AttestationInput<'a> {
42 pub rid: &'a str,
44 pub identity_did: &'a IdentityDID,
46 pub subject: &'a CanonicalDid,
52 pub device_public_key: &'a [u8],
54 pub device_curve: auths_crypto::CurveType,
57 pub payload: Option<Value>,
59 pub meta: &'a AttestationMetadata,
61 pub identity_alias: Option<&'a KeyAlias>,
63 pub device_alias: Option<&'a KeyAlias>,
65 pub delegated_by: Option<IdentityDID>,
67 pub commit_sha: Option<String>,
69 pub signer_type: Option<SignerType>,
71}
72
73pub fn create_signed_attestation(
90 now: DateTime<Utc>,
91 input: AttestationInput<'_>,
92 signer: &dyn SecureSigner,
93 passphrase_provider: &dyn PassphraseProvider,
94) -> Result<Attestation, AttestationError> {
95 let AttestationInput {
96 rid,
97 identity_did,
98 subject,
99 device_public_key,
100 device_curve,
101 payload,
102 meta,
103 identity_alias,
104 device_alias,
105 delegated_by,
106 commit_sha,
107 signer_type,
108 } = input;
109 let expected = device_curve.public_key_len();
112 if device_public_key.len() != expected {
113 return Err(AttestationError::InvalidInput(format!(
114 "Device public key length {} does not match {} (expected {} bytes)",
115 device_public_key.len(),
116 device_curve,
117 expected
118 )));
119 }
120
121 if let Some(ts) = meta.timestamp {
123 let drift = (now - ts).num_seconds().abs();
124 if drift > MAX_CREATION_SKEW_SECS {
125 return Err(AttestationError::InvalidInput(format!(
126 "System clock drift {}s exceeds {}s limit",
127 drift, MAX_CREATION_SKEW_SECS
128 )));
129 }
130 }
131
132 #[allow(clippy::disallowed_methods)]
134 let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str());
136 #[allow(clippy::disallowed_methods)]
137 let subject_canonical = CanonicalDid::new_unchecked(subject.as_str());
139 let delegated_canonical = delegated_by.as_ref().map(|d| CanonicalDid::from(d.clone()));
140
141 let mut attestation = Attestation {
142 version: ATTESTATION_VERSION,
143 subject: subject_canonical,
144 issuer: issuer_canonical,
145 rid: ResourceId::new(rid),
146 payload: payload.clone(),
147 timestamp: meta.timestamp,
148 expires_at: meta.expires_at,
149 revoked_at: None,
150 note: meta.note.clone(),
151 device_public_key: auths_verifier::DevicePublicKey::try_new(
152 device_curve,
153 device_public_key,
154 )
155 .map_err(|e| AttestationError::InvalidInput(e.to_string()))?,
156 identity_signature: Ed25519Signature::empty(),
157 device_signature: Ed25519Signature::empty(),
158 delegated_by: delegated_canonical,
159 signer_type,
160 environment_claim: None,
161 commit_sha,
162 commit_message: None,
163 author: None,
164 oidc_binding: None,
165 };
166
167 let message_to_sign = canonicalize_attestation_data(&attestation.canonical_data())?;
169
170 if let Some(alias) = identity_alias {
172 debug!("Signing attestation with identity alias '{}'", alias);
173 let sig = signer
174 .sign_with_alias(alias, passphrase_provider, &message_to_sign)
175 .map_err(|e| {
176 AttestationError::SigningError(format!(
177 "Failed to sign with identity key '{}': {}",
178 alias, e
179 ))
180 })?;
181 debug!("Identity signature obtained successfully");
182 attestation.identity_signature = Ed25519Signature::try_from_slice(&sig)
183 .map_err(|e| AttestationError::SigningError(e.to_string()))?;
184 } else {
185 debug!("No identity alias provided, skipping identity signature (device-only attestation)");
186 }
187
188 if let Some(alias) = device_alias {
190 debug!("Signing attestation with device alias '{}'", alias);
191 let sig = signer
192 .sign_with_alias(alias, passphrase_provider, &message_to_sign)
193 .map_err(|e| {
194 AttestationError::SigningError(format!(
195 "Failed to sign with device key '{}': {}",
196 alias, e
197 ))
198 })?;
199 debug!("Device signature obtained successfully");
200 attestation.device_signature = Ed25519Signature::try_from_slice(&sig)
201 .map_err(|e| AttestationError::SigningError(e.to_string()))?;
202 } else {
203 debug!("No device alias provided, skipping device signature");
204 }
205
206 Ok(attestation)
207}
208
209pub fn canonicalize_revocation_data(
211 data: &CanonicalRevocationData,
212) -> Result<Vec<u8>, AttestationError> {
213 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
214 AttestationError::SerializationError(format!(
215 "Failed to create canonical JSON for revocation: {}",
216 e
217 ))
218 })?;
219 debug!(
220 "Generated canonical data (revocation): {}",
221 canonical_json_string
222 );
223 Ok(canonical_json_string.into_bytes())
224}