crates/dwarves-slsa/src/dsse.rs
//! DSSE v1: the envelope, its PAE, and the ECDSA P-256 check against the release key.
use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use crate::{bail, Result};
/// The fixed DER prefix of a P-256 SubjectPublicKeyInfo (id-ecPublicKey,
/// prime256v1, a 66-byte BIT STRING); the 65-byte uncompressed point follows.
const P256_SPKI_PREFIX: [u8; 26] = [
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a,
0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00,
];
/// The DSSE v1 pre-authentication encoding:
/// `"DSSEv1" SP len(type) SP type SP len(body) SP body`.
pub fn pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
let mut out = format!(
"DSSEv1 {} {} {} ",
payload_type.len(),
payload_type,
payload.len()
)
.into_bytes();
out.extend_from_slice(payload);
out
}
/// One signature.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature {
#[serde(default)]
pub keyid: String,
/// base64 of the ASN.1 DER ECDSA signature over the PAE (SHA-256).
pub sig: String,
}
/// A DSSE envelope, as `.intoto.jsonl` carries it (one per line).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Envelope {
#[serde(rename = "payloadType")]
pub payload_type: String,
/// base64 of the payload.
pub payload: String,
pub signatures: Vec<Signature>,
}
impl Envelope {
/// The decoded payload.
pub fn payload_bytes(&self) -> Result<Vec<u8>> {
B64.decode(self.payload.trim())
.map_err(|e| crate::Error(format!("the envelope payload is not base64: {e}")))
}
/// One compact JSON line, newline-terminated.
pub fn to_line(&self) -> String {
let mut s = serde_json::to_string(self).unwrap_or_default();
s.push('\n');
s
}
}
/// Read a bundle: JSON lines, one envelope each (a single pretty envelope too).
pub fn parse_bundle(text: &str) -> Result<Vec<Envelope>> {
if text.trim().is_empty() {
bail!("the bundle is empty");
}
if let Ok(one) = serde_json::from_str::<Envelope>(text) {
return Ok(vec![one]);
}
let mut out = Vec::new();
for (i, l) in text
.lines()
.enumerate()
.filter(|(_, l)| !l.trim().is_empty())
{
out.push(serde_json::from_str::<Envelope>(l).map_err(|e| {
crate::Error(format!("bundle line {}: not a DSSE envelope: {e}", i + 1))
})?);
}
Ok(out)
}
/// The DER of a PEM `PUBLIC KEY` block (or bare base64).
pub fn spki_from_pem(text: &str) -> Result<Vec<u8>> {
let body: String = text
.lines()
.filter(|l| !l.starts_with("-----"))
.map(str::trim)
.collect();
let der = B64
.decode(body)
.map_err(|e| crate::Error(format!("the public key is not PEM or base64: {e}")))?;
p256_point(&der)?;
Ok(der)
}
/// The uncompressed point of a P-256 SubjectPublicKeyInfo; anything else refused.
pub fn p256_point(spki: &[u8]) -> Result<&[u8]> {
if spki.len() != 91 || spki[..26] != P256_SPKI_PREFIX || spki[26] != 0x04 {
bail!("the key is not an ECDSA P-256 SubjectPublicKeyInfo (the release key is P-256)");
}
Ok(&spki[26..])
}
/// The key id this platform writes: hex sha256 of the SubjectPublicKeyInfo DER.
pub fn keyid(spki: &[u8]) -> String {
crate::subjects::sha256_hex(spki)
}
/// ★ The index of the first signature that verifies with the key; refused when none does.
pub fn verify_p256(env: &Envelope, spki: &[u8]) -> Result<usize> {
use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1};
let point = p256_point(spki)?;
let msg = pae(&env.payload_type, &env.payload_bytes()?);
let key = UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, point);
for (i, s) in env.signatures.iter().enumerate() {
let Ok(sig) = B64.decode(s.sig.trim()) else {
continue;
};
if key.verify(&msg, &sig).is_ok() {
return Ok(i);
}
}
bail!(
"none of the envelope's {} signature(s) verifies with the release key {}",
env.signatures.len(),
keyid(spki)
)
}