use std::sync::Arc;
use auths_core::signing::{SecureSigner, StorageSigner};
use auths_core::storage::keychain::KeyAlias;
use auths_crypto::CurveType;
use auths_keri::KeriPublicKey;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use serde::{Deserialize, Serialize};
use crate::context::AuthsContext;
use crate::domains::compliance::frameworks::FrameworkReport;
use crate::domains::compliance::query::{ComplianceQueryError, EvidencePack};
pub const DSSE_INTOTO_PAYLOAD_TYPE: &str = "application/vnd.in-toto+json";
fn pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len() + payload_type.len() + 32);
out.extend_from_slice(b"DSSEv1 ");
out.extend_from_slice(payload_type.len().to_string().as_bytes());
out.push(b' ');
out.extend_from_slice(payload_type.as_bytes());
out.push(b' ');
out.extend_from_slice(payload.len().to_string().as_bytes());
out.push(b' ');
out.extend_from_slice(payload);
out
}
fn curve_tag(curve: CurveType) -> &'static str {
match curve {
CurveType::Ed25519 => "ed25519",
CurveType::P256 => "p256",
}
}
fn curve_from_tag(tag: &str) -> CurveType {
match tag {
"ed25519" => CurveType::Ed25519,
_ => CurveType::P256,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DsseSignature {
pub keyid: String,
pub curve: String,
pub sig: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DsseEnvelope {
#[serde(rename = "payloadType")]
pub payload_type: String,
pub payload: String,
pub signatures: Vec<DsseSignature>,
}
impl DsseEnvelope {
pub fn decoded_payload(&self) -> Result<Vec<u8>, ComplianceQueryError> {
BASE64
.decode(self.payload.as_bytes())
.map_err(|e| ComplianceQueryError::Decode(format!("dsse payload base64: {e}")))
}
pub fn to_canonical_json(&self) -> Result<String, ComplianceQueryError> {
json_canon::to_string(self).map_err(|e| ComplianceQueryError::Canonicalize(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, ComplianceQueryError> {
serde_json::from_str(json).map_err(|e| ComplianceQueryError::Decode(e.to_string()))
}
pub fn verify(
&self,
org_public_key: &[u8],
org_curve: CurveType,
) -> Result<(), ComplianceQueryError> {
let payload = self.decoded_payload()?;
let to_verify = pae(&self.payload_type, &payload);
let key = KeriPublicKey::from_verkey_bytes(org_public_key, org_curve)
.map_err(|e| ComplianceQueryError::Verification(e.to_string()))?;
for s in &self.signatures {
if curve_from_tag(&s.curve) != org_curve {
continue;
}
let sig = BASE64
.decode(s.sig.as_bytes())
.map_err(|e| ComplianceQueryError::Decode(format!("dsse sig base64: {e}")))?;
if key.verify_signature(&to_verify, &sig).is_ok() {
return Ok(());
}
}
Err(ComplianceQueryError::Verification(
"no DSSE signature verified against the org key".into(),
))
}
}
pub fn sign_evidence_pack(
ctx: &AuthsContext,
org_did: &str,
org_alias: &KeyAlias,
org_curve: CurveType,
pack: &EvidencePack,
) -> Result<DsseEnvelope, ComplianceQueryError> {
sign_intoto_statement(
ctx,
org_did,
org_alias,
org_curve,
&pack.to_intoto_statement()?,
)
}
pub fn sign_framework_report(
ctx: &AuthsContext,
org_did: &str,
org_alias: &KeyAlias,
org_curve: CurveType,
report: &FrameworkReport,
) -> Result<DsseEnvelope, ComplianceQueryError> {
sign_intoto_statement(
ctx,
org_did,
org_alias,
org_curve,
&report.to_intoto_statement()?,
)
}
fn sign_intoto_statement(
ctx: &AuthsContext,
org_did: &str,
org_alias: &KeyAlias,
org_curve: CurveType,
statement: &str,
) -> Result<DsseEnvelope, ComplianceQueryError> {
let payload = statement.as_bytes();
let to_sign = pae(DSSE_INTOTO_PAYLOAD_TYPE, payload);
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let sig = signer
.sign_with_alias(org_alias, ctx.passphrase_provider.as_ref(), &to_sign)
.map_err(|e| ComplianceQueryError::Signing(e.to_string()))?;
Ok(DsseEnvelope {
payload_type: DSSE_INTOTO_PAYLOAD_TYPE.to_string(),
payload: BASE64.encode(payload),
signatures: vec![DsseSignature {
keyid: org_did.to_string(),
curve: curve_tag(org_curve).to_string(),
sig: BASE64.encode(&sig),
}],
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn pae_matches_dsse_spec_layout() {
let got = pae("demo", b"hello");
assert_eq!(got, b"DSSEv1 4 demo 5 hello");
}
#[test]
fn pae_is_length_prefixed_not_delimiter_ambiguous() {
let a = pae("t", b"a b");
let b = pae("t", b"a b");
assert_ne!(
a, b,
"PAE length-prefix must distinguish differing payloads"
);
}
#[test]
fn curve_tag_round_trips() {
assert_eq!(
curve_from_tag(curve_tag(CurveType::Ed25519)),
CurveType::Ed25519
);
assert_eq!(curve_from_tag(curve_tag(CurveType::P256)), CurveType::P256);
assert_eq!(curve_from_tag("unknown"), CurveType::P256);
}
}