uselesskey-core 0.8.0

Core factory, deterministic derivation, and cache engine for uselesskey test fixtures.
Documentation
//! PKCS#8 / SPKI key-material helpers shared by key fixture crates.
//!
//! Provides the `Pkcs8SpkiKeyMaterial` trait and related types for consistent
//! access to private (PKCS#8) and public (SPKI) key encodings in PEM and DER
//! formats, plus corrupt PEM/DER negative fixture support.

use std::fmt;
use std::sync::Arc;

use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;

use crate::Error;
use crate::negative::{
    CorruptPem, corrupt_der_deterministic, corrupt_pem, corrupt_pem_deterministic, truncate_der,
};
use crate::sink::TempArtifact;

/// Common PKCS#8/SPKI key material shared by multiple fixture crates.
#[derive(Clone)]
pub struct Pkcs8SpkiKeyMaterial {
    pkcs8_der: Arc<[u8]>,
    pkcs8_pem: String,
    spki_der: Arc<[u8]>,
    spki_pem: String,
}

impl fmt::Debug for Pkcs8SpkiKeyMaterial {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Pkcs8SpkiKeyMaterial")
            .field("pkcs8_der_len", &self.pkcs8_der.len())
            .field("pkcs8_pem_len", &self.pkcs8_pem.len())
            .field("spki_der_len", &self.spki_der.len())
            .field("spki_pem_len", &self.spki_pem.len())
            .finish_non_exhaustive()
    }
}

impl Pkcs8SpkiKeyMaterial {
    /// Build a material container from PKCS#8 and SPKI forms.
    pub fn new(
        pkcs8_der: impl Into<Arc<[u8]>>,
        pkcs8_pem: impl Into<String>,
        spki_der: impl Into<Arc<[u8]>>,
        spki_pem: impl Into<String>,
    ) -> Self {
        Self {
            pkcs8_der: pkcs8_der.into(),
            pkcs8_pem: pkcs8_pem.into(),
            spki_der: spki_der.into(),
            spki_pem: spki_pem.into(),
        }
    }

    /// PKCS#8 DER-encoded private key bytes.
    pub fn private_key_pkcs8_der(&self) -> &[u8] {
        &self.pkcs8_der
    }

    /// PKCS#8 PEM-encoded private key.
    pub fn private_key_pkcs8_pem(&self) -> &str {
        &self.pkcs8_pem
    }

    /// SPKI DER-encoded public key bytes.
    pub fn public_key_spki_der(&self) -> &[u8] {
        &self.spki_der
    }

    /// SPKI PEM-encoded public key.
    pub fn public_key_spki_pem(&self) -> &str {
        &self.spki_pem
    }

    /// Write the PKCS#8 PEM private key to a tempfile and return the handle.
    pub fn write_private_key_pkcs8_pem(&self) -> Result<TempArtifact, Error> {
        TempArtifact::new_string("uselesskey-", ".pkcs8.pem", self.private_key_pkcs8_pem())
    }

    /// Write the SPKI PEM public key to a tempfile and return the handle.
    pub fn write_public_key_spki_pem(&self) -> Result<TempArtifact, Error> {
        TempArtifact::new_string("uselesskey-", ".spki.pem", self.public_key_spki_pem())
    }

    /// Produce a corrupted variant of the PKCS#8 PEM.
    pub fn private_key_pkcs8_pem_corrupt(&self, how: CorruptPem) -> String {
        corrupt_pem(self.private_key_pkcs8_pem(), how)
    }

    /// Produce a deterministic corrupted PKCS#8 PEM using a variant string.
    pub fn private_key_pkcs8_pem_corrupt_deterministic(&self, variant: &str) -> String {
        corrupt_pem_deterministic(self.private_key_pkcs8_pem(), variant)
    }

    /// Produce a truncated variant of the PKCS#8 DER.
    pub fn private_key_pkcs8_der_truncated(&self, len: usize) -> Vec<u8> {
        truncate_der(self.private_key_pkcs8_der(), len)
    }

    /// Produce a deterministic corrupted PKCS#8 DER using a variant string.
    pub fn private_key_pkcs8_der_corrupt_deterministic(&self, variant: &str) -> Vec<u8> {
        corrupt_der_deterministic(self.private_key_pkcs8_der(), variant)
    }

    /// A stable key identifier derived from the SPKI bytes.
    pub fn kid(&self) -> String {
        kid_from_bytes(self.public_key_spki_der())
    }
}

const DEFAULT_KID_PREFIX_BYTES: usize = 12;

fn kid_from_bytes(bytes: &[u8]) -> String {
    let digest = blake3::hash(bytes);
    URL_SAFE_NO_PAD.encode(&digest.as_bytes()[..DEFAULT_KID_PREFIX_BYTES])
}

#[cfg(test)]
mod tests {
    use super::Pkcs8SpkiKeyMaterial;
    use crate::negative::CorruptPem;
    use uselesskey_test_support::{TestResult, require_ok};

    fn sample_material() -> Pkcs8SpkiKeyMaterial {
        Pkcs8SpkiKeyMaterial::new(
            vec![0x30, 0x82, 0x01, 0x22],
            "-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n".to_string(),
            vec![0x30, 0x59, 0x30, 0x13],
            "-----BEGIN PUBLIC KEY-----\nBBBB\n-----END PUBLIC KEY-----\n".to_string(),
        )
    }

    #[test]
    fn accessors_expose_material() {
        let material = sample_material();

        assert_eq!(material.private_key_pkcs8_der(), &[0x30, 0x82, 0x01, 0x22]);
        assert!(
            material
                .private_key_pkcs8_pem()
                .contains("BEGIN PRIVATE KEY")
        );
        assert_eq!(material.public_key_spki_der(), &[0x30, 0x59, 0x30, 0x13]);
        assert!(material.public_key_spki_pem().contains("BEGIN PUBLIC KEY"));
    }

    #[test]
    fn debug_does_not_include_key_pem() {
        let material = sample_material();
        let dbg = format!("{material:?}");
        assert!(dbg.contains("Pkcs8SpkiKeyMaterial"));
        assert!(!dbg.contains("BEGIN PRIVATE KEY"));
    }

    #[test]
    fn private_key_pkcs8_pem_corrupt() {
        let material = sample_material();
        let corrupted = material.private_key_pkcs8_pem_corrupt(CorruptPem::BadHeader);
        assert_ne!(corrupted, material.private_key_pkcs8_pem());
        assert!(corrupted.contains("CORRUPTED KEY"));
    }

    #[test]
    fn deterministic_corruption_is_stable() {
        let material = sample_material();
        let a = material.private_key_pkcs8_pem_corrupt_deterministic("core-keypair:v1");
        let b = material.private_key_pkcs8_pem_corrupt_deterministic("core-keypair:v1");
        assert_eq!(a, b);
        assert_ne!(a, material.private_key_pkcs8_pem());
        // Must still look like (corrupted) PEM, not a constant like "" or "xyzzy"
        assert!(a.contains("-----"));
    }

    #[test]
    fn truncation_respects_requested_length() {
        let material = sample_material();
        let truncated = material.private_key_pkcs8_der_truncated(2);
        assert_eq!(truncated.len(), 2);
        assert_eq!(truncated, &material.private_key_pkcs8_der()[..2]);
    }

    #[test]
    fn private_key_pkcs8_der_corrupt_deterministic() {
        let material = sample_material();
        let a = material.private_key_pkcs8_der_corrupt_deterministic("variant-a");
        let b = material.private_key_pkcs8_der_corrupt_deterministic("variant-a");
        assert_eq!(a, b);
        assert_ne!(a, material.private_key_pkcs8_der());
        // Different variants must produce different corruption — a constant return can't satisfy this
        let c = material.private_key_pkcs8_der_corrupt_deterministic("variant-b");
        assert_ne!(a, c);
    }

    #[test]
    fn kid_is_deterministic() {
        let material = sample_material();
        let a = material.kid();
        let b = material.kid();
        assert_eq!(a, b);
        assert!(!a.is_empty());
    }

    #[test]
    fn kid_depends_on_spki_bytes() {
        let m1 = sample_material();
        let m2 = Pkcs8SpkiKeyMaterial::new(
            vec![0x30, 0x82, 0x01, 0x22],
            "-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
            vec![0xFF, 0xFE, 0xFD, 0xFC],
            "-----BEGIN PUBLIC KEY-----\nCCCC\n-----END PUBLIC KEY-----\n",
        );
        assert_ne!(m1.kid(), m2.kid());
    }

    #[test]
    fn tempfile_writers_round_trip_content() -> TestResult<()> {
        let material = sample_material();

        let private = require_ok(material.write_private_key_pkcs8_pem(), "write private")?;
        let public = require_ok(material.write_public_key_spki_pem(), "write public")?;

        let private_text = require_ok(private.read_to_string(), "read private")?;
        let public_text = require_ok(public.read_to_string(), "read public")?;

        assert!(private_text.contains("BEGIN PRIVATE KEY"));
        assert!(public_text.contains("BEGIN PUBLIC KEY"));
        Ok(())
    }

    mod property {
        use super::Pkcs8SpkiKeyMaterial;
        use super::sample_material;

        use proptest::prelude::*;

        fn sample_material_with_der(der: Vec<u8>) -> Pkcs8SpkiKeyMaterial {
            Pkcs8SpkiKeyMaterial::new(
                der,
                sample_material().private_key_pkcs8_pem(),
                sample_material().public_key_spki_der(),
                sample_material().public_key_spki_pem(),
            )
        }

        proptest! {
            #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]

            #[test]
            fn truncation_len_is_capped(
                der in prop::collection::vec(any::<u8>(), 0..128),
                request in 0usize..256,
            ) {
                let material = sample_material_with_der(der.clone());
                let truncated = material.private_key_pkcs8_der_truncated(request);
                assert_eq!(truncated.len(), request.min(der.len()));
            }

            #[test]
            fn deterministic_pem_corruption_is_reproducible(
                seed in "[a-zA-Z0-9]{1,24}",
            ) {
                let material = sample_material();
                let a = material.private_key_pkcs8_pem_corrupt_deterministic(&seed);
                let b = material.private_key_pkcs8_pem_corrupt_deterministic(&seed);
                assert_eq!(a, b);
            }

            #[test]
            fn kid_stable_for_fixed_spki(
                private_pem in "[A-Z ]{0,64}",
            ) {
                let material = Pkcs8SpkiKeyMaterial::new(
                    vec![0x30, 0x82, 0x01, 0x22],
                    private_pem,
                    vec![0x30, 0x59, 0x30, 0x13],
                    "-----BEGIN PUBLIC KEY-----\nBBBB\n-----END PUBLIC KEY-----\n".to_string(),
                );
                let a = material.kid();
                let b = material.kid();
                prop_assert!(!a.is_empty());
                assert_eq!(a, b);
            }
        }
    }
}