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