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 issuer: &'a CanonicalDid,
48 pub subject: &'a CanonicalDid,
54 pub device_public_key: &'a [u8],
56 pub device_curve: auths_crypto::CurveType,
59 pub payload: Option<Value>,
61 pub meta: &'a AttestationMetadata,
63 pub identity_alias: Option<&'a KeyAlias>,
65 pub device_alias: Option<&'a KeyAlias>,
67 pub delegated_by: Option<IdentityDID>,
69 pub commit_sha: Option<String>,
71 pub signer_type: Option<SignerType>,
73 pub oidc_binding: Option<auths_verifier::core::OidcBinding>,
77}
78
79pub fn create_signed_attestation(
96 now: DateTime<Utc>,
97 input: AttestationInput<'_>,
98 signer: &dyn SecureSigner,
99 passphrase_provider: &dyn PassphraseProvider,
100) -> Result<Attestation, AttestationError> {
101 let AttestationInput {
102 rid,
103 issuer,
104 subject,
105 device_public_key,
106 device_curve,
107 payload,
108 meta,
109 identity_alias,
110 device_alias,
111 delegated_by,
112 commit_sha,
113 signer_type,
114 oidc_binding,
115 } = input;
116 let expected = device_curve.public_key_len();
119 if device_public_key.len() != expected {
120 return Err(AttestationError::InvalidInput(format!(
121 "Device public key length {} does not match {} (expected {} bytes)",
122 device_public_key.len(),
123 device_curve,
124 expected
125 )));
126 }
127
128 if let Some(ts) = meta.timestamp {
130 let drift = (now - ts).num_seconds().abs();
131 if drift > MAX_CREATION_SKEW_SECS {
132 return Err(AttestationError::InvalidInput(format!(
133 "System clock drift {}s exceeds {}s limit",
134 drift, MAX_CREATION_SKEW_SECS
135 )));
136 }
137 }
138
139 let issuer_canonical = issuer.clone();
142 #[allow(clippy::disallowed_methods)]
143 let subject_canonical = CanonicalDid::new_unchecked(subject.as_str());
145 let delegated_canonical = delegated_by.as_ref().map(|d| CanonicalDid::from(d.clone()));
146
147 let mut attestation = Attestation {
148 version: ATTESTATION_VERSION,
149 subject: subject_canonical,
150 issuer: issuer_canonical,
151 rid: ResourceId::new(rid),
152 payload: payload.clone(),
153 timestamp: meta.timestamp,
154 expires_at: meta.expires_at,
155 revoked_at: None,
156 note: meta.note.clone(),
157 device_public_key: auths_verifier::DevicePublicKey::try_new(
158 device_curve,
159 device_public_key,
160 )
161 .map_err(|e| AttestationError::InvalidInput(e.to_string()))?,
162 identity_signature: Ed25519Signature::empty(),
163 device_signature: Ed25519Signature::empty(),
164 delegated_by: delegated_canonical,
165 signer_type,
166 environment_claim: None,
167 commit_sha,
168 commit_message: None,
169 author: None,
170 oidc_binding,
171 };
172
173 let message_to_sign = canonicalize_attestation_data(&attestation.canonical_data())?;
175
176 if let Some(alias) = identity_alias {
178 debug!("Signing attestation with identity alias '{}'", alias);
179 let sig = signer
180 .sign_with_alias(alias, passphrase_provider, &message_to_sign)
181 .map_err(|e| {
182 AttestationError::SigningError(format!(
183 "Failed to sign with identity key '{}': {}",
184 alias, e
185 ))
186 })?;
187 debug!("Identity signature obtained successfully");
188 attestation.identity_signature = Ed25519Signature::try_from_slice(&sig)
189 .map_err(|e| AttestationError::SigningError(e.to_string()))?;
190 } else {
191 debug!("No identity alias provided, skipping identity signature (device-only attestation)");
192 }
193
194 if let Some(alias) = device_alias {
196 debug!("Signing attestation with device alias '{}'", alias);
197 let sig = signer
198 .sign_with_alias(alias, passphrase_provider, &message_to_sign)
199 .map_err(|e| {
200 AttestationError::SigningError(format!(
201 "Failed to sign with device key '{}': {}",
202 alias, e
203 ))
204 })?;
205 debug!("Device signature obtained successfully");
206 attestation.device_signature = Ed25519Signature::try_from_slice(&sig)
207 .map_err(|e| AttestationError::SigningError(e.to_string()))?;
208 } else {
209 debug!("No device alias provided, skipping device signature");
210 }
211
212 Ok(attestation)
213}
214
215pub fn canonicalize_revocation_data(
217 data: &CanonicalRevocationData,
218) -> Result<Vec<u8>, AttestationError> {
219 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
220 AttestationError::SerializationError(format!(
221 "Failed to create canonical JSON for revocation: {}",
222 e
223 ))
224 })?;
225 debug!(
226 "Generated canonical data (revocation): {}",
227 canonical_json_string
228 );
229 Ok(canonical_json_string.into_bytes())
230}