Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

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)
    )
}