use anyhow::{bail, Context, Result};
use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};
use pgp::types::KeyDetails;
pub const SIGNING_KEY: &str = include_str!("../release-signing-key.asc");
pub const SIGNING_KEY_FINGERPRINT: &str = "C0197617BAE752019693A17E95377BC8B27FF227";
pub fn signing_key() -> Result<SignedPublicKey> {
let (key, _headers) = SignedPublicKey::from_string(SIGNING_KEY)
.context("the embedded release signing key is malformed")?;
let actual = format!("{:x}", key.fingerprint());
if !actual.eq_ignore_ascii_case(SIGNING_KEY_FINGERPRINT) {
bail!(
"embedded signing key {actual} is not the pinned key {}",
SIGNING_KEY_FINGERPRINT.to_lowercase()
);
}
Ok(key)
}
pub fn signature(data: &[u8], armoured: &str) -> Result<()> {
let key = signing_key()?;
let (signature, _headers) =
DetachedSignature::from_string(armoured).context("the .asc signature file is malformed")?;
signature
.verify(&key.primary_key, data)
.context("the checksums file does not match the Accent CMS release signing key")
}
pub fn checksum(expected: &str, actual: &str) -> Result<()> {
if !expected.eq_ignore_ascii_case(actual) {
bail!("checksum mismatch\n expected: {expected}\n actual: {actual}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const CHECKSUMS: &str = include_str!("../tests/fixtures/checksums-v0.25.0.txt");
const SIGNATURE: &str = include_str!("../tests/fixtures/checksums-v0.25.0.txt.asc");
#[test]
fn the_embedded_key_is_the_pinned_one() {
signing_key().unwrap();
}
#[test]
fn accepts_a_real_release_signature() {
signature(CHECKSUMS.as_bytes(), SIGNATURE).unwrap();
}
#[test]
fn rejects_a_tampered_checksums_file() {
let tampered = CHECKSUMS.replacen("98e5", "98e6", 1);
assert_ne!(tampered, CHECKSUMS);
assert!(signature(tampered.as_bytes(), SIGNATURE).is_err());
}
#[test]
fn rejects_a_signature_that_is_not_a_signature() {
assert!(signature(CHECKSUMS.as_bytes(), "not a signature").is_err());
}
#[test]
fn checksum_comparison_ignores_case_only() {
let digest = "98e505528fac8256353fd8cf27f4017dd3da3580424fed9f95d4d0e1e4b4b30d";
checksum(digest, &digest.to_uppercase()).unwrap();
assert!(checksum(digest, &digest.replace("98e5", "98e6")).is_err());
}
}