#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::{
crypto::CryptoBackend,
error::PqRascvError,
measurement::{Measurements, RoT},
provenance::InTotoAttestation,
};
pub const PROTOCOL_VERSION: u16 = 1;
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct QuoteBody {
pub version: u16,
pub timestamp: u64,
pub nonce: [u8; 32],
pub measurements: Measurements,
pub provenance: InTotoAttestation,
pub pub_key_id: [u8; 32],
}
#[cfg(feature = "alloc")]
impl QuoteBody {
pub fn to_cbor(&self) -> Result<Vec<u8>, PqRascvError> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf)
.map_err(|_| PqRascvError::SerializationFailed)?;
Ok(buf)
}
}
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct AttestationQuote {
pub body: QuoteBody,
#[serde(with = "serde_bytes")]
pub signature: Vec<u8>,
}
#[cfg(feature = "alloc")]
impl AttestationQuote {
pub fn to_cbor(&self) -> Result<Vec<u8>, PqRascvError> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf)
.map_err(|_| PqRascvError::SerializationFailed)?;
Ok(buf)
}
pub fn from_cbor(bytes: &[u8]) -> Result<Self, PqRascvError> {
ciborium::from_reader(bytes).map_err(|_| PqRascvError::DeserializationFailed)
}
}
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Challenge {
pub nonce: [u8; 32],
pub policy_id: Option<alloc::string::String>,
}
#[cfg(feature = "alloc")]
impl Challenge {
#[must_use]
pub fn new(nonce: [u8; 32]) -> Self {
Self { nonce, policy_id: None }
}
#[must_use]
pub fn with_policy(mut self, policy_id: impl Into<alloc::string::String>) -> Self {
self.policy_id = Some(policy_id.into());
self
}
}
#[cfg(feature = "alloc")]
pub fn generate_quote<R: RoT, C: CryptoBackend>(
rot: &R,
crypto: &C,
signing_seed: &[u8],
verifying_key: &[u8],
nonce: &[u8; 32],
provenance: InTotoAttestation,
timestamp: u64,
) -> Result<AttestationQuote, PqRascvError> {
let measurements = rot.measure()?;
let pub_key_id = C::pub_key_id(verifying_key);
let body = QuoteBody {
version: PROTOCOL_VERSION,
timestamp,
nonce: *nonce,
measurements,
provenance,
pub_key_id,
};
let body_cbor = body.to_cbor()?;
let sig = crypto.sign(&body_cbor, signing_seed)?;
Ok(AttestationQuote {
body,
signature: sig.as_ref().to_vec(),
})
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
use crate::{
crypto::{generate_ml_dsa_keypair, MlDsaBackend},
measurement::SoftwareRoT,
provenance::SlsaPredicateBuilder,
};
fn test_provenance() -> InTotoAttestation {
SlsaPredicateBuilder::new("https://ci.test")
.add_subject("fw.bin", &[0xabu8; 32])
.with_slsa_level(1)
.build()
.unwrap()
}
#[test]
fn generate_quote_succeeds() {
let (seed, vk) = generate_ml_dsa_keypair().unwrap();
let rot = SoftwareRoT::new(b"test-firmware", None, 1);
let nonce = [0x42u8; 32];
let quote = generate_quote(
&rot,
&MlDsaBackend,
seed.as_bytes(),
&vk,
&nonce,
test_provenance(),
1_700_000_000,
)
.unwrap();
assert_eq!(quote.body.version, PROTOCOL_VERSION);
assert_eq!(quote.body.nonce, nonce);
assert_eq!(quote.signature.len(), crate::crypto::ML_DSA_65_SIGNATURE_SIZE);
}
#[test]
fn quote_cbor_roundtrip() {
let (seed, vk) = generate_ml_dsa_keypair().unwrap();
let rot = SoftwareRoT::new(b"fw", None, 0);
let original = generate_quote(
&rot,
&MlDsaBackend,
seed.as_bytes(),
&vk,
&[0x01u8; 32],
test_provenance(),
0,
)
.unwrap();
let cbor = original.to_cbor().unwrap();
let decoded = AttestationQuote::from_cbor(&cbor).unwrap();
assert_eq!(original.body.nonce, decoded.body.nonce);
assert_eq!(original.signature, decoded.signature);
}
#[test]
fn quote_signature_verifies() {
let (seed, vk) = generate_ml_dsa_keypair().unwrap();
let rot = SoftwareRoT::new(b"fw", None, 0);
let quote = generate_quote(
&rot,
&MlDsaBackend,
seed.as_bytes(),
&vk,
&[0x99u8; 32],
test_provenance(),
0,
)
.unwrap();
let body_cbor = quote.body.to_cbor().unwrap();
MlDsaBackend
.verify(&body_cbor, &vk, "e.signature)
.expect("signature must verify");
}
}