use crate::storage::git_refs::AttestationMetadata;
use auths_core::signing::{PassphraseProvider, SecureSigner};
use auths_core::storage::keychain::{IdentityDID, KeyAlias};
use auths_verifier::core::{
Attestation, Ed25519Signature, ResourceId, SignerType, canonicalize_attestation_data,
};
use auths_verifier::error::AttestationError;
use auths_verifier::types::CanonicalDid;
use chrono::{DateTime, Utc};
use log::debug;
use serde::Serialize;
use serde_json::Value;
pub const ATTESTATION_VERSION: u32 = 1;
const MAX_CREATION_SKEW_SECS: i64 = 5 * 60;
#[derive(Serialize, Debug)] pub struct CanonicalRevocationData<'a> {
pub version: u32,
pub rid: &'a str,
pub issuer: &'a CanonicalDid,
pub subject: &'a CanonicalDid,
pub timestamp: &'a Option<DateTime<Utc>>,
pub revoked_at: &'a Option<DateTime<Utc>>, pub note: &'a Option<String>,
}
pub struct AttestationInput<'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 payload: Option<Value>,
pub meta: &'a AttestationMetadata,
pub identity_alias: Option<&'a KeyAlias>,
pub device_alias: Option<&'a KeyAlias>,
pub delegated_by: Option<IdentityDID>,
pub commit_sha: Option<String>,
pub signer_type: Option<SignerType>,
}
pub fn create_signed_attestation(
now: DateTime<Utc>,
input: AttestationInput<'_>,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
) -> Result<Attestation, AttestationError> {
let AttestationInput {
rid,
identity_did,
subject,
device_public_key,
device_curve,
payload,
meta,
identity_alias,
device_alias,
delegated_by,
commit_sha,
signer_type,
} = input;
let expected = device_curve.public_key_len();
if device_public_key.len() != expected {
return Err(AttestationError::InvalidInput(format!(
"Device public key length {} does not match {} (expected {} bytes)",
device_public_key.len(),
device_curve,
expected
)));
}
if let Some(ts) = meta.timestamp {
let drift = (now - ts).num_seconds().abs();
if drift > MAX_CREATION_SKEW_SECS {
return Err(AttestationError::InvalidInput(format!(
"System clock drift {}s exceeds {}s limit",
drift, MAX_CREATION_SKEW_SECS
)));
}
}
#[allow(clippy::disallowed_methods)]
let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str());
#[allow(clippy::disallowed_methods)]
let subject_canonical = CanonicalDid::new_unchecked(subject.as_str());
let delegated_canonical = delegated_by.as_ref().map(|d| CanonicalDid::from(d.clone()));
let mut attestation = Attestation {
version: ATTESTATION_VERSION,
subject: subject_canonical,
issuer: issuer_canonical,
rid: ResourceId::new(rid),
payload: payload.clone(),
timestamp: meta.timestamp,
expires_at: meta.expires_at,
revoked_at: None,
note: meta.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: Ed25519Signature::empty(),
device_signature: Ed25519Signature::empty(),
delegated_by: delegated_canonical,
signer_type,
environment_claim: None,
commit_sha,
commit_message: None,
author: None,
oidc_binding: None,
};
let message_to_sign = canonicalize_attestation_data(&attestation.canonical_data())?;
if let Some(alias) = identity_alias {
debug!("Signing attestation with identity alias '{}'", alias);
let sig = signer
.sign_with_alias(alias, passphrase_provider, &message_to_sign)
.map_err(|e| {
AttestationError::SigningError(format!(
"Failed to sign with identity key '{}': {}",
alias, e
))
})?;
debug!("Identity signature obtained successfully");
attestation.identity_signature = Ed25519Signature::try_from_slice(&sig)
.map_err(|e| AttestationError::SigningError(e.to_string()))?;
} else {
debug!("No identity alias provided, skipping identity signature (device-only attestation)");
}
if let Some(alias) = device_alias {
debug!("Signing attestation with device alias '{}'", alias);
let sig = signer
.sign_with_alias(alias, passphrase_provider, &message_to_sign)
.map_err(|e| {
AttestationError::SigningError(format!(
"Failed to sign with device key '{}': {}",
alias, e
))
})?;
debug!("Device signature obtained successfully");
attestation.device_signature = Ed25519Signature::try_from_slice(&sig)
.map_err(|e| AttestationError::SigningError(e.to_string()))?;
} else {
debug!("No device alias provided, skipping device signature");
}
Ok(attestation)
}
pub fn canonicalize_revocation_data(
data: &CanonicalRevocationData,
) -> Result<Vec<u8>, AttestationError> {
let canonical_json_string = json_canon::to_string(data).map_err(|e| {
AttestationError::SerializationError(format!(
"Failed to create canonical JSON for revocation: {}",
e
))
})?;
debug!(
"Generated canonical data (revocation): {}",
canonical_json_string
);
Ok(canonical_json_string.into_bytes())
}