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

nordisk/holger

provenance/src/crypto.rs

//! Public keys and signature checks. Verify paths only, through `ring`.

use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use ring::signature::{self, UnparsedPublicKey};
use sha2::{Digest, Sha256};

use crate::der;
use crate::error::{ProvenanceError, Result};

const ID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
const P256: &str = "1.2.840.10045.3.1.7";
const P384: &str = "1.3.132.0.34";
const ED25519: &str = "1.3.101.112";
const RSA: &str = "1.2.840.113549.1.1.1";

/// The hash a signature was made over, where the key type leaves a choice.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Hash {
    Sha256,
    Sha384,
    Sha1,
}

/// A public key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PublicKey {
    /// 32 raw bytes.
    Ed25519(Vec<u8>),
    /// The uncompressed point.
    EcdsaP256(Vec<u8>),
    EcdsaP384(Vec<u8>),
    /// The RSAPublicKey DER.
    Rsa(Vec<u8>),
}

impl PublicKey {
    /// Read a SubjectPublicKeyInfo DER.
    pub fn from_spki_der(spki: &[u8]) -> Result<Self> {
        let bad = |w: &str| ProvenanceError::Malformed(format!("public key: {w}"));
        let (seq, _) = der::read(spki)?;
        let kids = der::children(seq.content)?;
        if kids.len() != 2 || kids[1].tag != 0x03 || kids[1].content.is_empty() {
            return Err(bad("not a SubjectPublicKeyInfo"));
        }
        let alg = der::children(kids[0].content)?;
        let oid = alg
            .first()
            .filter(|t| t.tag == 0x06)
            .ok_or_else(|| bad("no algorithm"))?;
        let key = kids[1].content[1..].to_vec();
        let is = |d: &str| oid.content == der::oid_bytes(d).as_slice();
        if is(ED25519) {
            return Ok(PublicKey::Ed25519(key));
        }
        if is(RSA) {
            return Ok(PublicKey::Rsa(key));
        }
        if is(ID_EC_PUBLIC_KEY) {
            let curve = alg
                .get(1)
                .filter(|t| t.tag == 0x06)
                .ok_or_else(|| bad("an EC key names no curve"))?;
            if curve.content == der::oid_bytes(P256).as_slice() {
                return Ok(PublicKey::EcdsaP256(key));
            }
            if curve.content == der::oid_bytes(P384).as_slice() {
                return Ok(PublicKey::EcdsaP384(key));
            }
            return Err(bad("an EC curve other than P-256 or P-384"));
        }
        Err(bad(
            "an algorithm other than Ed25519, ECDSA P-256/P-384 or RSA",
        ))
    }

    /// Read a PEM `PUBLIC KEY` block, or the bare base64 of a SubjectPublicKeyInfo.
    pub fn from_text(text: &str) -> Result<Self> {
        Self::from_spki_der(&pem_or_b64(text)?)
    }

    /// The algorithm's name, for the record.
    pub fn alg(&self) -> &'static str {
        match self {
            PublicKey::Ed25519(_) => "ed25519",
            PublicKey::EcdsaP256(_) => "ecdsa-p256",
            PublicKey::EcdsaP384(_) => "ecdsa-p384",
            PublicKey::Rsa(_) => "rsa",
        }
    }

    /// Verify `sig` over `msg` (the key's natural hash).
    pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool {
        let hash = match self {
            PublicKey::EcdsaP384(_) => Hash::Sha384,
            _ => Hash::Sha256,
        };
        self.verify_with(hash, msg, sig)
    }

    /// Verify `sig` over `msg` with the given hash. ECDSA signatures are ASN.1
    /// DER; RSA is PKCS#1 v1.5.
    pub fn verify_with(&self, hash: Hash, msg: &[u8], sig: &[u8]) -> bool {
        let alg: &'static dyn signature::VerificationAlgorithm = match (self, hash) {
            (PublicKey::Ed25519(_), _) => &signature::ED25519,
            (PublicKey::EcdsaP256(_), Hash::Sha256) => &signature::ECDSA_P256_SHA256_ASN1,
            (PublicKey::EcdsaP256(_), Hash::Sha384) => &signature::ECDSA_P256_SHA384_ASN1,
            (PublicKey::EcdsaP384(_), Hash::Sha256) => &signature::ECDSA_P384_SHA256_ASN1,
            (PublicKey::EcdsaP384(_), Hash::Sha384) => &signature::ECDSA_P384_SHA384_ASN1,
            (PublicKey::Rsa(_), Hash::Sha256) => &signature::RSA_PKCS1_2048_8192_SHA256,
            (PublicKey::Rsa(_), Hash::Sha384) => &signature::RSA_PKCS1_2048_8192_SHA384,
            (PublicKey::Rsa(_), Hash::Sha1) => {
                &signature::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY
            }
            _ => return false,
        };
        let raw = match self {
            PublicKey::Ed25519(k)
            | PublicKey::EcdsaP256(k)
            | PublicKey::EcdsaP384(k)
            | PublicKey::Rsa(k) => k,
        };
        UnparsedPublicKey::new(alg, raw).verify(msg, sig).is_ok()
    }
}

/// Decode a PEM block (any label) or bare base64.
pub fn pem_or_b64(text: &str) -> Result<Vec<u8>> {
    let body: String = if text.contains("-----BEGIN") {
        text.lines()
            .filter(|l| !l.starts_with("-----"))
            .map(str::trim)
            .collect()
    } else {
        text.split_whitespace().collect()
    };
    B64.decode(body.as_bytes())
        .map_err(|e| ProvenanceError::Malformed(format!("base64: {e}")))
}

/// Every PEM `CERTIFICATE` block in `text`, as DER.
pub fn pem_certs(text: &str) -> Result<Vec<Vec<u8>>> {
    let mut out = Vec::new();
    let mut cur: Option<String> = None;
    for line in text.lines() {
        let l = line.trim();
        if l.starts_with("-----BEGIN") {
            cur = Some(String::new());
        } else if l.starts_with("-----END") {
            if let Some(b) = cur.take() {
                out.push(
                    B64.decode(b.as_bytes())
                        .map_err(|e| ProvenanceError::Malformed(format!("PEM: {e}")))?,
                );
            }
        } else if let Some(b) = cur.as_mut() {
            b.push_str(l);
        }
    }
    Ok(out)
}

/// Standard base64 decode, named for the field it came from.
pub fn b64(field: &str, s: &str) -> Result<Vec<u8>> {
    B64.decode(s.trim().as_bytes())
        .map_err(|e| ProvenanceError::Malformed(format!("{field} is not base64: {e}")))
}

/// Standard base64 encode.
pub fn b64_encode(bytes: &[u8]) -> String {
    B64.encode(bytes)
}

/// SHA-256.
pub fn sha256(bytes: &[u8]) -> [u8; 32] {
    Sha256::digest(bytes).into()
}

/// SHA-256, lowercase hex.
pub fn sha256_hex(bytes: &[u8]) -> String {
    hex::encode(sha256(bytes))
}

/// The SHA-256 of a key's SPKI DER: Sigstore's log id and CT log id.
pub fn key_id(spki_der: &[u8]) -> [u8; 32] {
    sha256(spki_der)
}