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;
#[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 {
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(),
}
}
pub fn private_key_pkcs8_der(&self) -> &[u8] {
&self.pkcs8_der
}
pub fn private_key_pkcs8_pem(&self) -> &str {
&self.pkcs8_pem
}
pub fn public_key_spki_der(&self) -> &[u8] {
&self.spki_der
}
pub fn public_key_spki_pem(&self) -> &str {
&self.spki_pem
}
pub fn write_private_key_pkcs8_pem(&self) -> Result<TempArtifact, Error> {
TempArtifact::new_string("uselesskey-", ".pkcs8.pem", self.private_key_pkcs8_pem())
}
pub fn write_public_key_spki_pem(&self) -> Result<TempArtifact, Error> {
TempArtifact::new_string("uselesskey-", ".spki.pem", self.public_key_spki_pem())
}
pub fn private_key_pkcs8_pem_corrupt(&self, how: CorruptPem) -> String {
corrupt_pem(self.private_key_pkcs8_pem(), how)
}
pub fn private_key_pkcs8_pem_corrupt_deterministic(&self, variant: &str) -> String {
corrupt_pem_deterministic(self.private_key_pkcs8_pem(), variant)
}
pub fn private_key_pkcs8_der_truncated(&self, len: usize) -> Vec<u8> {
truncate_der(self.private_key_pkcs8_der(), len)
}
pub fn private_key_pkcs8_der_corrupt_deterministic(&self, variant: &str) -> Vec<u8> {
corrupt_der_deterministic(self.private_key_pkcs8_der(), variant)
}
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());
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());
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);
}
}
}
}