rebuilderd 0.27.0

rebuilderd - independent build verification daemon
Documentation
use in_toto::{
    crypto::{KeyType, PrivateKey, PublicKey, SignatureScheme},
    models::{Metablock, MetadataWrapper},
};
use pem::Pem;
use rebuilderd_common::errors::*;
use rebuilderd_common::utils;
use rebuilderd_common::utils::{is_zstd_compressed, zstd_compress, zstd_decompress};
use std::borrow::Cow;
use std::path::Path;

const PEM_PUBLIC_KEY: &str = "PUBLIC KEY";
const PEM_PRIVATE_KEY: &str = "PRIVATE KEY";

pub struct Secret(Vec<u8>);

fn keygen() -> Result<(Secret, PublicKey)> {
    let privkey = PrivateKey::new(KeyType::Ed25519)?;

    let pubkey = {
        let privkey = PrivateKey::from_pkcs8(&privkey, SignatureScheme::Ed25519)?;
        privkey.public().to_owned()
    };

    Ok((Secret(privkey), pubkey))
}

pub fn keygen_pem() -> Result<(String, String)> {
    let (privkey, pubkey) = keygen()?;

    let privkey = privkey_to_pem(privkey);
    let pubkey = pubkey_to_pem(&pubkey)?;

    Ok((privkey, pubkey))
}

pub fn privkey_to_pem(privkey: Secret) -> String {
    pem::encode(&Pem::new(PEM_PRIVATE_KEY, privkey.0))
}

pub fn pubkey_to_pem(pubkey: &PublicKey) -> Result<String> {
    let pubkey = pubkey.as_spki()?;
    let pem = pem::encode(&Pem::new(PEM_PUBLIC_KEY, pubkey));
    Ok(pem)
}

pub fn pem_to_privkeys(buf: &[u8]) -> Result<impl Iterator<Item = Result<PrivateKey>>> {
    let pems = pem::parse_many(buf).context("Failed to parse pem file")?;
    let iter = pems
        .into_iter()
        .filter(|pem| pem.tag() == PEM_PRIVATE_KEY)
        .map(|pem| {
            PrivateKey::from_pkcs8(pem.contents(), SignatureScheme::Ed25519)
                .context("Failed to parse private key")
        });
    Ok(iter)
}

pub fn pem_to_pubkeys(buf: &[u8]) -> Result<impl Iterator<Item = Result<PublicKey>>> {
    let pems = pem::parse_many(buf).context("Failed to parse pem file")?;
    let iter = pems
        .into_iter()
        .filter(|pem| pem.tag() == PEM_PUBLIC_KEY)
        .map(|pem| {
            PublicKey::from_spki(pem.contents(), SignatureScheme::Ed25519)
                .context("Failed to parse public key")
        });
    Ok(iter)
}

pub fn load_or_create_privkey_pem(path: &Path) -> Result<PrivateKey> {
    let privkey = utils::load_or_create(path, || {
        info!("generating new signing private key: {path:?}");
        let privkey = PrivateKey::new(KeyType::Ed25519)?;
        let pem = privkey_to_pem(Secret(privkey));
        Ok(pem.into_bytes())
    })?;

    pem_to_privkeys(&privkey)?
        .next()
        .context("No private key found in PEM file")?
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attestation {
    pub metablock: Metablock,
}

impl Attestation {
    pub fn parse(bytes: &[u8]) -> Result<Self> {
        let metablock = serde_json::from_slice::<Metablock>(bytes)?;
        Ok(Self { metablock })
    }

    pub fn has_signature(&self, pubkey: &PublicKey) -> bool {
        self.metablock
            .signatures
            .iter()
            .any(|sig| sig.key_id() == pubkey.key_id())
    }

    pub fn sign(&mut self, privkey: &PrivateKey) -> Result<()> {
        debug!("creating signature on attestation");
        let new = Metablock::new(self.metablock.metadata.clone(), &[privkey])?;
        self.metablock.signatures.extend(new.signatures);
        Ok(())
    }

    pub fn verify<'a, I>(&self, threshold: u32, authorized_keys: I) -> Result<MetadataWrapper>
    where
        I: IntoIterator<Item = &'a PublicKey>,
    {
        let metadata = self.metablock.verify(threshold, authorized_keys)?;
        Ok(metadata)
    }

    pub fn serialize(&self) -> Result<String> {
        serde_json::to_string(&self.metablock).context("Failed to serialize attestation")
    }

    pub async fn to_compressed_bytes(&self) -> Result<Vec<u8>> {
        let json = self.serialize()?;
        let compressed = zstd_compress(json.as_bytes()).await?;
        Ok(compressed)
    }
}

/// Makes sure the attestation is signed by our private key
/// Returns true if a signature was created, returns false if attestation was already signed by us
pub async fn compressed_attestation_sign_if_necessary(
    bytes: Vec<u8>,
    privkey: &PrivateKey,
) -> Result<(Vec<u8>, bool)> {
    let decompressed = if is_zstd_compressed(&bytes) {
        let decompressed = zstd_decompress(&bytes).await.map_err(Error::from)?;
        Cow::Owned(decompressed)
    } else {
        Cow::Borrowed(&bytes)
    };

    let mut attestation = Attestation::parse(&decompressed)?;
    if attestation.has_signature(privkey.public()) {
        Ok((bytes, false))
    } else {
        attestation.sign(privkey)?;

        let compressed = attestation.to_compressed_bytes().await?;
        Ok((compressed, true))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use data_encoding::HEXLOWER;
    use in_toto::{
        crypto::{HashAlgorithm, HashValue, KeyType, Signature, SignatureScheme},
        models::{LinkMetadata, MetadataWrapper, VirtualTargetPath},
    };
    use serde_json::Value;

    // temporary until https://github.com/in-toto/in-toto-rs/pull/111 lands
    fn hashvalue_from_hex(hex: &str) -> Result<HashValue> {
        let bytes = HEXLOWER.decode(hex.as_bytes())?;
        Ok(HashValue::new(bytes))
    }

    // temporary until https://github.com/in-toto/in-toto-rs/pull/111 lands
    fn signature(keyid: &str, value: &str) -> Signature {
        let value = Value::Object(
            [
                ("keyid".to_string(), Value::String(keyid.to_string())),
                ("sig".to_string(), Value::String(value.to_string())),
            ]
            .into_iter()
            .collect(),
        );
        serde_json::from_value(value).unwrap()
    }

    #[test]
    fn test_parse() {
        let json = r#"{"signatures":[{"keyid":"c25d24c04760b6982de77736776edc6600d5f8e1e84d0bba2a7299959ce7d47f","sig":"8cd70318ea1b34c91bf7303e9c8811df43d1b4746aa9adf1d503ebb0241e0fbff9be28f36dac0318825782bf05dbbcea7171eb0ca9a89be3b02666f0f3c84301"}],"signed":{"_type":"link","name":"rebuild spytrap-adb_0.3.5-1_amd64.deb","materials":{"rust-spytrap-adb_0.3.5-1_amd64.buildinfo":{"sha512":"d130dbdbd51480f5cb79c1e6ce09fa61a69766e56725543b9c19bee8248306b2c3c2a2c66b250992bf20b2f5af7cf03bf401255104714bc9d654126fb41bc59f","sha256":"9df2f9a721f5016874c5f78ae88d3df77f9e49ea6070f935bfeeb438cd73a158"}},"products":{"spytrap-adb_0.3.5-1_amd64.deb":{"sha256":"58a7d451d5d59fda6284a05418b99e34fab32d07e63d0b164404eaaed1317edd","sha512":"f38806536701138cb1b2059565e5f73ec07288f9a3013ba986e33d510432e183e7bfe94af31bb8d480b85c84f4c145ed5c28c5949d618a4e94b2c7aecb309642"}},"environment":null,"byproducts":{},"command":[]}}"#;
        let metablock = Attestation::parse(json.as_bytes()).unwrap();
        assert_eq!(metablock, Attestation {
            metablock: Metablock {
                signatures: vec![signature(
                    "c25d24c04760b6982de77736776edc6600d5f8e1e84d0bba2a7299959ce7d47f",
                    "8cd70318ea1b34c91bf7303e9c8811df43d1b4746aa9adf1d503ebb0241e0fbff9be28f36dac0318825782bf05dbbcea7171eb0ca9a89be3b02666f0f3c84301",
                )],
                metadata: MetadataWrapper::Link(LinkMetadata {
                    name: "rebuild spytrap-adb_0.3.5-1_amd64.deb".to_string(),
                    materials: [
                        (VirtualTargetPath::new("rust-spytrap-adb_0.3.5-1_amd64.buildinfo".to_string()).unwrap(), [
                            (HashAlgorithm::Sha512, hashvalue_from_hex("d130dbdbd51480f5cb79c1e6ce09fa61a69766e56725543b9c19bee8248306b2c3c2a2c66b250992bf20b2f5af7cf03bf401255104714bc9d654126fb41bc59f").unwrap()),
                            (HashAlgorithm::Sha256, hashvalue_from_hex("9df2f9a721f5016874c5f78ae88d3df77f9e49ea6070f935bfeeb438cd73a158").unwrap()),
                        ].into_iter().collect()),
                    ].into_iter().collect(),
                    products: [
                        (VirtualTargetPath::new("spytrap-adb_0.3.5-1_amd64.deb".to_string()).unwrap(), [
                            (HashAlgorithm::Sha256, hashvalue_from_hex("58a7d451d5d59fda6284a05418b99e34fab32d07e63d0b164404eaaed1317edd").unwrap()),
                            (HashAlgorithm::Sha512, hashvalue_from_hex("f38806536701138cb1b2059565e5f73ec07288f9a3013ba986e33d510432e183e7bfe94af31bb8d480b85c84f4c145ed5c28c5949d618a4e94b2c7aecb309642").unwrap()),
                        ].into_iter().collect()),
                    ].into_iter().collect(),
                    env: None,
                    byproducts: Default::default(),
                    command: vec![].into(),
                })
            }
        });
    }

    #[test]
    fn test_append_signature() {
        // generate keypair
        let privkey = PrivateKey::new(KeyType::Ed25519).unwrap();
        let privkey = PrivateKey::from_pkcs8(&privkey, SignatureScheme::Ed25519).unwrap();
        let pubkey = privkey.public();

        // take a metablock
        let json = r#"{"signatures":[{"keyid":"c25d24c04760b6982de77736776edc6600d5f8e1e84d0bba2a7299959ce7d47f","sig":"8cd70318ea1b34c91bf7303e9c8811df43d1b4746aa9adf1d503ebb0241e0fbff9be28f36dac0318825782bf05dbbcea7171eb0ca9a89be3b02666f0f3c84301"}],"signed":{"_type":"link","name":"rebuild spytrap-adb_0.3.5-1_amd64.deb","materials":{"rust-spytrap-adb_0.3.5-1_amd64.buildinfo":{"sha512":"d130dbdbd51480f5cb79c1e6ce09fa61a69766e56725543b9c19bee8248306b2c3c2a2c66b250992bf20b2f5af7cf03bf401255104714bc9d654126fb41bc59f","sha256":"9df2f9a721f5016874c5f78ae88d3df77f9e49ea6070f935bfeeb438cd73a158"}},"products":{"spytrap-adb_0.3.5-1_amd64.deb":{"sha256":"58a7d451d5d59fda6284a05418b99e34fab32d07e63d0b164404eaaed1317edd","sha512":"f38806536701138cb1b2059565e5f73ec07288f9a3013ba986e33d510432e183e7bfe94af31bb8d480b85c84f4c145ed5c28c5949d618a4e94b2c7aecb309642"}},"environment":null,"byproducts":{},"command":[]}}"#;
        let mut attestation = Attestation::parse(json.as_bytes()).unwrap();

        // ensure it's not valid yet
        attestation.verify(1, [pubkey]).unwrap_err();
        assert!(!attestation.has_signature(pubkey));

        // append a signature with our key
        attestation.sign(&privkey).unwrap();

        // ensure it's valid now
        attestation.verify(1, [pubkey]).unwrap();
        assert!(attestation.has_signature(pubkey));
    }

    #[test]
    fn test_load_privkey() {
        let mut iter = pem_to_privkeys(
            b"-----BEGIN PRIVATE KEY-----
            MFECAQEwBQYDK2VwBCIEINOWEV/DNN+AsZ+pLoixusXNmgS5x0TNXvkLQUnKz92k
            gSEAB5ySaw+WE9Ut06fYlPf2V4+5gbFHA5HZJK7n2WWAGvA=
            -----END PRIVATE KEY-----
            ",
        )
        .unwrap();
        let privkey = iter.next().unwrap().unwrap();
        let pubkey = pubkey_to_pem(privkey.public()).unwrap();
        assert_eq!(
            pubkey,
            "-----BEGIN PUBLIC KEY-----\r\n\
        MCwwBwYDK2VwBQADIQAHnJJrD5YT1S3Tp9iU9/ZXj7mBsUcDkdkkrufZZYAa8A==\r\n\
        -----END PUBLIC KEY-----\r\n\
        "
        );
    }
}