vauban-claim 0.1.0

Vauban Claim Algebra — reference implementation of draft-vauban-claim-algebra-00 (post-quantum claim sextuplet + 5 composition operators, canonical CBOR/JSON codec).
Documentation
//! Evidence primitive (CDDL §5.3).

use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};

use crate::error::CompositionError;

/// Evidence scheme tag.
///
/// Aligned with CDDL `evidence-scheme`. SNARKs (Groth16, PLONK, Marlin)
/// are explicitly excluded per the proof-system policy.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EvidenceScheme {
    /// Circle-STARK / Stwo proof — recommended.
    Stark,
    /// BBS+ signature.
    BbsPlus,
    /// SD-JWT-VC selective disclosure.
    SdJwt,
    /// ISO/IEC 18013-5 mdoc IssuerSigned.
    Mdoc,
    /// TLS-Notary attestation.
    TlsNotary,
    /// Intel TDX attestation.
    TeeTdx,
    /// AMD SEV-SNP attestation.
    TeeSevSnp,
    /// Ed25519 digital signature — self-describing 64-byte proof, no envelope.
    /// Phase-0 trusted-platform attestations (e.g. Vauban Finance patrimoine).
    /// Migration path: swap for `Stark` (ZK threshold proof) in Phase 2+.
    Ed25519,
}

/// Evidence (CDDL §5.3) — the cryptographic artefact making the Predicate certain.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Evidence {
    /// Scheme discriminator.
    pub scheme: EvidenceScheme,
    /// Scheme-specific proof bytes.
    #[serde(with = "serde_bytes")]
    pub proof: Vec<u8>,
    /// Public inputs the Verifier must bind into its Fiat-Shamir transcript.
    #[serde(rename = "public-inputs", default, skip_serializing_if = "Option::is_none")]
    pub public_inputs: Option<Vec<ByteString>>,
    /// Optional verifier-key reference.
    #[serde(rename = "verifier-key", default, skip_serializing_if = "Option::is_none")]
    pub verifier_key: Option<ByteString>,
    /// Optional discriminated envelope per scheme (CDDL `evidence-envelope`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub envelope: Option<EvidenceEnvelope>,
}

/// Newtype wrapping `Vec<u8>` so serde emits CBOR bstr for nested byte fields.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ByteString(pub Vec<u8>);

impl Serialize for ByteString {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_bytes(&self.0)
    }
}
impl<'de> Deserialize<'de> for ByteString {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let bytes = serde_bytes::ByteBuf::deserialize(d)?;
        Ok(Self(bytes.into_vec()))
    }
}

/// Discriminated envelope variants per scheme (CDDL `evidence-envelope`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EvidenceEnvelope {
    /// Circle-STARK / Stwo envelope.
    Stark(StarkProofEnvelope),
    /// BBS+ envelope.
    BbsPlus(BbsPlusEnvelope),
    /// SD-JWT envelope.
    SdJwt(SdJwtEnvelope),
    /// mdoc envelope.
    Mdoc(MdocEnvelope),
    /// TLS-Notary envelope.
    TlsNotary(TlsNotaryEnvelope),
    /// TEE attestation envelope.
    Tee(TeeAttestationEnvelope),
}

/// Circle-STARK / Stwo envelope (CDDL `stark-proof-envelope`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StarkProofEnvelope {
    /// Discriminator literal `"stark"`.
    pub scheme: String,
    /// Proof-system version pair.
    pub version: u64,
    /// Raw proof bytes.
    #[serde(with = "serde_bytes")]
    pub proof: Vec<u8>,
    /// Public inputs (LE field-element bytes).
    #[serde(rename = "public-inputs")]
    pub public_inputs: Vec<ByteString>,
    /// Optional verifier parameters.
    #[serde(rename = "verifier-params", default, skip_serializing_if = "Option::is_none")]
    pub verifier_params: Option<ByteString>,
    /// Poseidon transcript domain separator (32B).
    #[serde(rename = "transcript-tag", default, skip_serializing_if = "Option::is_none")]
    pub transcript_tag: Option<ByteString>,
}

/// BBS+ envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BbsPlusEnvelope {
    /// Discriminator literal.
    pub scheme: String,
    /// BBS+ signature bytes.
    #[serde(with = "serde_bytes")]
    pub signature: Vec<u8>,
    /// Indices of disclosed messages.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub disclosed: Option<Vec<u64>>,
    /// Optional nonce.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub nonce: Option<ByteString>,
}

/// SD-JWT envelope (RFC drafts).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SdJwtEnvelope {
    /// Discriminator.
    pub scheme: String,
    /// Compact JWS serialisation.
    pub jwt: String,
    /// Salted disclosures.
    pub disclosures: Vec<String>,
    /// Optional Key-Binding JWT.
    #[serde(rename = "key-binding", default, skip_serializing_if = "Option::is_none")]
    pub key_binding: Option<String>,
}

/// ISO/IEC 18013-5 mdoc envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MdocEnvelope {
    /// Discriminator.
    pub scheme: String,
    /// Raw IssuerSigned CBOR bytes.
    #[serde(rename = "issuer-signed", with = "serde_bytes")]
    pub issuer_signed: Vec<u8>,
    /// Optional DeviceSigned bytes.
    #[serde(rename = "device-signed", default, skip_serializing_if = "Option::is_none")]
    pub device_signed: Option<ByteString>,
}

/// TLS-Notary envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TlsNotaryEnvelope {
    /// Discriminator.
    pub scheme: String,
    /// Notary session header.
    #[serde(rename = "session-header", with = "serde_bytes")]
    pub session_header: Vec<u8>,
    /// Substring inclusion proof.
    #[serde(rename = "substring-proof", with = "serde_bytes")]
    pub substring_proof: Vec<u8>,
    /// Optional notary public key.
    #[serde(rename = "notary-pubkey", default, skip_serializing_if = "Option::is_none")]
    pub notary_pubkey: Option<ByteString>,
}

/// TEE attestation envelope (TDX or SEV-SNP).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TeeAttestationEnvelope {
    /// Discriminator (`"tee-tdx"` or `"tee-sev-snp"`).
    pub scheme: String,
    /// Raw quote bytes.
    #[serde(with = "serde_bytes")]
    pub quote: Vec<u8>,
    /// Optional verification collateral.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub collateral: Option<ByteString>,
}

impl Evidence {
    /// Scheme tag as string (for transcript absorption).
    pub fn scheme_tag(&self) -> &str {
        match self.scheme {
            EvidenceScheme::Stark => "stark",
            EvidenceScheme::BbsPlus => "bbs-plus",
            EvidenceScheme::SdJwt => "sd-jwt",
            EvidenceScheme::Mdoc => "mdoc",
            EvidenceScheme::TlsNotary => "tls-notary",
            EvidenceScheme::TeeTdx => "tee-tdx",
            EvidenceScheme::TeeSevSnp => "tee-sev-snp",
            EvidenceScheme::Ed25519 => "ed25519",
        }
    }
    /// Raw proof bytes.
    pub fn proof(&self) -> &[u8] { &self.proof }
    /// Public inputs, if present.
    pub fn public_inputs(&self) -> Option<&[ByteString]> { self.public_inputs.as_deref() }
    /// Verifier key reference, if present.
    pub fn verifier_key(&self) -> Option<&ByteString> { self.verifier_key.as_ref() }

    /// Construct an Evidence and validate scheme/envelope coherence (I-2 hook).
    pub fn new(
        scheme: EvidenceScheme,
        proof: Vec<u8>,
        envelope: Option<EvidenceEnvelope>,
    ) -> Result<Self, CompositionError> {
        let e = Self {
            scheme,
            proof,
            public_inputs: None,
            verifier_key: None,
            envelope,
        };
        e.validate_shape()?;
        Ok(e)
    }

    /// Validate proof bytes are non-empty and envelope discriminator (if present)
    /// matches the scheme.
    pub fn validate_shape(&self) -> Result<(), CompositionError> {
        if self.proof.is_empty() {
            return Err(CompositionError::Invariant(
                "evidence.proof must be non-empty",
            ));
        }
        match (&self.scheme, &self.envelope) {
            (EvidenceScheme::Stark, Some(EvidenceEnvelope::Stark(_)))
            | (EvidenceScheme::BbsPlus, Some(EvidenceEnvelope::BbsPlus(_)))
            | (EvidenceScheme::SdJwt, Some(EvidenceEnvelope::SdJwt(_)))
            | (EvidenceScheme::Mdoc, Some(EvidenceEnvelope::Mdoc(_)))
            | (EvidenceScheme::TlsNotary, Some(EvidenceEnvelope::TlsNotary(_)))
            | (EvidenceScheme::TeeTdx | EvidenceScheme::TeeSevSnp, Some(EvidenceEnvelope::Tee(_)))
            | (_, None) => Ok(()),
            _ => Err(CompositionError::Invariant(
                "evidence.envelope variant must match scheme",
            )),
        }
    }
}