use std::sync::Arc;
use rcgen::{
BasicConstraints, CertificateParams, CertificateSigningRequestParams, DistinguishedName,
DnType, DnValue, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SerialNumber,
};
use time::{Duration, OffsetDateTime};
use crate::auth::{certificate_der, fingerprint_of_certificate, Fingerprint};
use crate::control::ControlStore;
use crate::doc::DocRef;
use crate::error::{Error, Result};
use crate::model::{now_ms, UnixMillis};
use gunnar_store::secret::ISSUER_PRIVATE_KEY;
use gunnar_store::{SecretStore, SecretStoreExt};
pub const ISSUER_REF: &str = "refs/gunnar/pki";
pub const DEFAULT_ISSUER_DAYS: u32 = 3650;
pub const DEFAULT_LEAF_DAYS: u32 = 365;
const SERIAL_LEN: usize = 20;
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IssuerOrigin {
Created,
Imported,
}
impl std::fmt::Display for IssuerOrigin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
IssuerOrigin::Created => "created",
IssuerOrigin::Imported => "imported",
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Issuer {
pub subject: String,
pub cert_pem: String,
pub fingerprint: Fingerprint,
pub created: UnixMillis,
pub origin: IssuerOrigin,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TrustedCa {
pub label: String,
pub cert_pem: String,
pub fingerprint: Fingerprint,
pub added: UnixMillis,
pub is_issuer: bool,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Issued {
pub principal_id: String,
pub common_name: String,
pub cert_pem: String,
pub cert_der: Vec<u8>,
pub fingerprint: Fingerprint,
pub key_pem: Option<String>,
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct PkiDoc {
#[serde(default, skip_serializing_if = "Option::is_none")]
issuer: Option<Issuer>,
#[serde(default)]
trusted: Vec<TrustedCa>,
}
impl crate::doc::Doc for PkiDoc {
fn bind(&mut self, _store: &Arc<dyn ControlStore>) {}
fn flush(&mut self) -> Result<()> {
Ok(())
}
}
#[derive(Clone)]
pub struct Pki {
doc: DocRef<PkiDoc>,
secrets: Arc<dyn SecretStore>,
}
impl std::fmt::Debug for Pki {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Pki")
}
}
impl Pki {
pub fn new(control: Arc<dyn ControlStore>, secrets: Arc<dyn SecretStore>) -> Result<Self> {
Ok(Pki {
doc: DocRef::new(control, ISSUER_REF)?,
secrets,
})
}
pub fn read_only(control: Arc<dyn ControlStore>) -> Result<Self> {
Pki::new(control, Arc::new(NoSecrets))
}
pub fn ref_name(&self) -> &str {
self.doc.ref_name()
}
pub fn issuer(&self) -> Result<Option<Issuer>> {
Ok(self.doc.load()?.issuer.clone())
}
pub fn trusted_cas(&self) -> Result<Vec<TrustedCa>> {
Ok(self.doc.load()?.trusted.clone())
}
pub fn create(&self, subject: &str, valid_days: u32) -> Result<Issuer> {
if let Some(existing) = self.issuer()? {
return Err(Error::IssuerExists {
subject: existing.subject,
fingerprint: existing.fingerprint.as_str().to_owned(),
});
}
let key_pem = self.ensure_issuer_key()?;
let key_pair = parse_key(&key_pem)?;
let mut params = CertificateParams::default();
params.distinguished_name = distinguished_name(subject);
params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
params.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::DigitalSignature,
];
params.serial_number = Some(random_serial()?);
let (not_before, not_after) = validity(valid_days)?;
params.not_before = not_before;
params.not_after = not_after;
let certificate = params
.self_signed(&key_pair)
.map_err(|err| Error::Pki(format!("self-signing the issuer: {err}")))?;
let cert_pem = certificate.pem();
let issuer = Issuer {
subject: subject.to_owned(),
fingerprint: fingerprint_of_certificate(certificate.der()),
cert_pem: cert_pem.clone(),
created: now_ms(),
origin: IssuerOrigin::Created,
};
self.install(issuer)
}
pub fn import(&self, cert_pem: &str, key_pem: &str) -> Result<Issuer> {
if let Some(existing) = self.issuer()? {
return Err(Error::IssuerExists {
subject: existing.subject,
fingerprint: existing.fingerprint.as_str().to_owned(),
});
}
let key_pair = parse_key(key_pem)?;
let der = certificate_der(cert_pem.as_bytes())?;
let spki = subject_public_key_info(&der)?;
if spki != rcgen::PublicKeyData::subject_public_key_info(&key_pair) {
return Err(Error::Pki(
"this certificate and this private key are not each other's: the certificate's \
SubjectPublicKeyInfo is not the public half of the key. Nothing was stored"
.to_owned(),
));
}
rcgen::Issuer::from_ca_cert_pem(cert_pem, &key_pair)
.map_err(|err| Error::Pki(format!("reading the imported issuer certificate: {err}")))?;
let subject =
certificate_common_name(&der)?.unwrap_or_else(|| "<no common name>".to_owned());
let issuer = Issuer {
subject,
fingerprint: fingerprint_of_certificate(&der),
cert_pem: cert_pem.to_owned(),
created: now_ms(),
origin: IssuerOrigin::Imported,
};
let installed = self.install(issuer)?;
self.secrets
.put(ISSUER_PRIVATE_KEY, key_pem.as_bytes())
.map_err(Error::from)?;
Ok(installed)
}
fn install(&self, issuer: Issuer) -> Result<Issuer> {
let entry = TrustedCa {
label: issuer.subject.clone(),
cert_pem: issuer.cert_pem.clone(),
fingerprint: issuer.fingerprint.clone(),
added: issuer.created,
is_issuer: true,
};
self.doc.update(|doc| {
if let Some(existing) = &doc.issuer {
return Err(Error::IssuerExists {
subject: existing.subject.clone(),
fingerprint: existing.fingerprint.as_str().to_owned(),
});
}
doc.issuer = Some(issuer.clone());
doc.trusted.retain(|ca| ca.fingerprint != entry.fingerprint);
doc.trusted.push(entry.clone());
Ok(issuer.clone())
})
}
pub fn trust_ca(&self, label: &str, cert_pem: &str) -> Result<TrustedCa> {
let der = certificate_der(cert_pem.as_bytes())?;
x509_parser::parse_x509_certificate(&der)
.map_err(|err| Error::Pki(format!("reading the CA certificate: {err}")))?;
let entry = TrustedCa {
label: label.to_owned(),
cert_pem: cert_pem.to_owned(),
fingerprint: fingerprint_of_certificate(&der),
added: now_ms(),
is_issuer: false,
};
self.doc.update(|doc| {
let is_issuer = doc
.issuer
.as_ref()
.is_some_and(|i| i.fingerprint == entry.fingerprint);
let mut entry = entry.clone();
entry.is_issuer = is_issuer;
doc.trusted.retain(|ca| ca.fingerprint != entry.fingerprint);
doc.trusted.push(entry.clone());
Ok(entry)
})
}
pub fn untrust_ca(&self, fingerprint: &Fingerprint) -> Result<Option<TrustedCa>> {
self.doc.update(|doc| {
let Some(at) = doc
.trusted
.iter()
.position(|ca| &ca.fingerprint == fingerprint)
else {
return Ok(None);
};
if doc.trusted[at].is_issuer {
return Err(Error::Pki(format!(
"{fingerprint} is this server's own issuer; removing it from the trusted \
list would leave a server minting certificates it refuses. Rotate the \
issuer first"
)));
}
Ok(Some(doc.trusted.remove(at)))
})
}
pub fn issue_from_csr(
&self,
common_name: &str,
csr_pem: &str,
valid_days: u32,
) -> Result<Issued> {
let mut request = CertificateSigningRequestParams::from_pem(csr_pem).map_err(|err| {
Error::Pki(format!(
"this is not a usable certificate signing request: {err}. A CSR that does not \
verify against its own public key proves nothing about who holds the private \
half, which is the only thing it exists to prove"
))
})?;
harden(&mut request.params, common_name, valid_days)?;
let issuer = self.signing_handle()?;
let certificate = request
.signed_by(&issuer)
.map_err(|err| Error::Pki(format!("signing the request for {common_name}: {err}")))?;
Ok(self.issued(
common_name,
certificate.pem(),
certificate.der().to_vec(),
None,
))
}
pub fn issue_with_server_key(&self, common_name: &str, valid_days: u32) -> Result<Issued> {
let key_pair = KeyPair::generate()
.map_err(|err| Error::Pki(format!("minting a key for {common_name}: {err}")))?;
let mut params = CertificateParams::default();
harden(&mut params, common_name, valid_days)?;
let issuer = self.signing_handle()?;
let certificate = params
.signed_by(&key_pair, &issuer)
.map_err(|err| Error::Pki(format!("signing a certificate for {common_name}: {err}")))?;
Ok(self.issued(
common_name,
certificate.pem(),
certificate.der().to_vec(),
Some(key_pair.serialize_pem()),
))
}
pub fn issue_server_identity(&self, hosts: &[String], valid_days: u32) -> Result<Issued> {
if hosts.is_empty() {
return Err(Error::Pki(
"a server certificate needs at least one host name or address for its subject \
alternative names; a SAN-less server certificate matches nothing a client dials"
.to_owned(),
));
}
let key_pair = KeyPair::generate()
.map_err(|err| Error::Pki(format!("minting a server key: {err}")))?;
let common_name = &hosts[0];
let mut params = CertificateParams::new(hosts.to_vec())
.map_err(|err| Error::Pki(format!("server certificate SANs {hosts:?}: {err}")))?;
params.distinguished_name = distinguished_name(common_name);
params.is_ca = IsCa::ExplicitNoCa;
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
params.use_authority_key_identifier_extension = true;
params.serial_number = Some(random_serial()?);
let now = OffsetDateTime::now_utc();
params.not_before = now - NOT_BEFORE_SLACK;
params.not_after = now + Duration::days(i64::from(valid_days));
let issuer = self.signing_handle()?;
let certificate = params
.signed_by(&key_pair, &issuer)
.map_err(|err| Error::Pki(format!("signing the server certificate: {err}")))?;
Ok(self.issued(
common_name,
certificate.pem(),
certificate.der().to_vec(),
Some(key_pair.serialize_pem()),
))
}
fn issued(
&self,
common_name: &str,
cert_pem: String,
cert_der: Vec<u8>,
key_pem: Option<String>,
) -> Issued {
Issued {
principal_id: common_name.to_owned(),
common_name: common_name.to_owned(),
fingerprint: fingerprint_of_certificate(&cert_der),
cert_pem,
cert_der,
key_pem,
}
}
fn signing_handle(&self) -> Result<rcgen::Issuer<'static, KeyPair>> {
let issuer = self.issuer()?.ok_or(Error::NoIssuer)?;
let key_pem = self
.secrets
.get(ISSUER_PRIVATE_KEY)
.map_err(Error::from)?
.ok_or_else(|| {
Error::Pki(format!(
"this server has an issuer certificate ({}) but no private key for it at \
{ISSUER_PRIVATE_KEY}. It can verify certificates and issue none",
issuer.subject
))
})?;
let key_pem = String::from_utf8(key_pem)
.map_err(|_| Error::Pki("the stored issuer key is not utf-8 PEM".to_owned()))?;
let key_pair = parse_key(&key_pem)?;
let der = certificate_der(issuer.cert_pem.as_bytes())?;
if subject_public_key_info(&der)?
!= rcgen::PublicKeyData::subject_public_key_info(&key_pair)
{
return Err(Error::Pki(
"the stored issuer certificate and the stored issuer key do not go together"
.to_owned(),
));
}
rcgen::Issuer::from_ca_cert_pem(&issuer.cert_pem, key_pair)
.map_err(|err| Error::Pki(format!("reading the stored issuer certificate: {err}")))
}
fn ensure_issuer_key(&self) -> Result<String> {
let bytes = self
.secrets
.get_or_create_with(ISSUER_PRIVATE_KEY, || {
tracing::info!(
key = ISSUER_PRIVATE_KEY,
"no issuer private key stored; minting one. It is sealed under the KEK and \
never enters the control database"
);
let pair = KeyPair::generate()
.map_err(|err| gunnar_store::Error::backend(format!("{err}")))?;
Ok(pair.serialize_pem().into_bytes())
})
.map_err(Error::from)?;
String::from_utf8(bytes)
.map_err(|_| Error::Pki("the stored issuer key is not utf-8 PEM".to_owned()))
}
}
struct NoSecrets;
impl NoSecrets {
fn refuse() -> gunnar_store::Error {
gunnar_store::Error::backend(
"this handle can read the issuer certificate and the trusted-CA list and cannot \
reach a private key. Issuing needs the sealed secret store, and this handle was \
built without one on purpose",
)
}
}
impl SecretStore for NoSecrets {
fn get(&self, _key: &str) -> gunnar_store::Result<Option<Vec<u8>>> {
Err(Self::refuse())
}
fn put(&self, _key: &str, _value: &[u8]) -> gunnar_store::Result<()> {
Err(Self::refuse())
}
fn delete(&self, _key: &str) -> gunnar_store::Result<()> {
Err(Self::refuse())
}
fn get_or_create(
&self,
_key: &str,
_mk: &mut dyn FnMut() -> gunnar_store::Result<Vec<u8>>,
) -> gunnar_store::Result<Vec<u8>> {
Err(Self::refuse())
}
}
fn harden(params: &mut CertificateParams, common_name: &str, valid_days: u32) -> Result<()> {
if common_name.is_empty() {
return Err(Error::Pki(
"a certificate needs a common name; the certificate-identity scheme \
takes the identity from it"
.to_owned(),
));
}
params.distinguished_name = distinguished_name(common_name);
params.subject_alt_names.clear();
params.is_ca = IsCa::ExplicitNoCa;
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
params.name_constraints = None;
params.custom_extensions.clear();
params.crl_distribution_points.clear();
params.use_authority_key_identifier_extension = true;
params.serial_number = Some(random_serial()?);
let (not_before, not_after) = validity(valid_days)?;
params.not_before = not_before;
params.not_after = not_after;
Ok(())
}
fn distinguished_name(common_name: &str) -> DistinguishedName {
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, common_name);
dn
}
pub fn common_name(dn: &DistinguishedName) -> Option<String> {
match dn.get(&DnType::CommonName)? {
DnValue::Utf8String(text) => Some(text.clone()),
DnValue::PrintableString(text) => Some(text.as_str().to_owned()),
DnValue::Ia5String(text) => Some(text.as_str().to_owned()),
DnValue::TeletexString(text) => Some(String::from_utf8_lossy(text.as_bytes()).into_owned()),
_ => None,
}
}
pub fn csr_claimed_common_name(csr_pem: &str) -> Result<Option<String>> {
let request = CertificateSigningRequestParams::from_pem(csr_pem)
.map_err(|err| Error::Pki(format!("this is not a usable request: {err}")))?;
Ok(common_name(&request.params.distinguished_name))
}
pub const NOT_BEFORE_SLACK: Duration = Duration::hours(24);
fn validity(valid_days: u32) -> Result<(OffsetDateTime, OffsetDateTime)> {
if valid_days == 0 {
return Err(Error::Pki(
"a certificate valid for zero days is expired the moment it is signed".to_owned(),
));
}
let now = OffsetDateTime::now_utc();
let until = now
.checked_add(Duration::days(i64::from(valid_days)))
.ok_or_else(|| Error::Pki(format!("{valid_days} days from now is not a date")))?;
Ok((now - NOT_BEFORE_SLACK, until))
}
fn random_serial() -> Result<SerialNumber> {
use aws_lc_rs::rand::{SecureRandom as _, SystemRandom};
let mut bytes = [0u8; SERIAL_LEN];
SystemRandom::new().fill(&mut bytes).map_err(|_| {
Error::Pki("the system CSPRNG refused to produce a serial number".to_owned())
})?;
bytes[0] &= 0x7f;
Ok(SerialNumber::from_slice(&bytes))
}
fn parse_key(pem: &str) -> Result<KeyPair> {
KeyPair::from_pem(pem)
.map_err(|err| Error::Pki(format!("this is not a usable private key: {err}")))
}
fn certificate_common_name(der: &[u8]) -> Result<Option<String>> {
let (_rest, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
let name = cert
.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(str::to_owned);
Ok(name)
}
fn subject_public_key_info(der: &[u8]) -> Result<Vec<u8>> {
let (_rest, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
Ok(cert.tbs_certificate.subject_pki.raw.to_vec())
}
pub fn mint_client_request(common_name: &str) -> Result<(String, String)> {
let key =
KeyPair::generate().map_err(|err| Error::Pki(format!("minting a key pair: {err}")))?;
let mut params = CertificateParams::new(Vec::<String>::new())
.map_err(|err| Error::Pki(format!("building a signing request: {err}")))?;
params.distinguished_name = DistinguishedName::new();
params
.distinguished_name
.push(DnType::CommonName, common_name);
let request = params
.serialize_request(&key)
.map_err(|err| Error::Pki(format!("signing the request with its own key: {err}")))?;
let pem = request
.pem()
.map_err(|err| Error::Pki(format!("armouring the signing request: {err}")))?;
Ok((key.serialize_pem(), pem))
}
pub fn public_key_info_of_key(key_pem: &str) -> Result<Vec<u8>> {
Ok(rcgen::PublicKeyData::subject_public_key_info(&parse_key(
key_pem,
)?))
}
pub fn public_key_info_of_cert(cert_pem: &str) -> Result<Vec<u8>> {
let (_rest, block) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
.map_err(|err| Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
subject_public_key_info(&block.contents)
}
pub fn public_key_info_of_der(cert_der: &[u8]) -> Result<Vec<u8>> {
subject_public_key_info(cert_der)
}
pub fn pem_to_der(pem: &str) -> Result<Vec<u8>> {
let (_rest, block) = x509_parser::pem::parse_x509_pem(pem.as_bytes())
.map_err(|err| Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
Ok(block.contents)
}
pub fn issued_by(
cert_pem: &str,
ca_pem: &str,
) -> Result<std::result::Result<(), (String, String)>> {
let (cert_der, ca_der) = (pem_to_der(cert_pem)?, pem_to_der(ca_pem)?);
let parse = |der: &[u8], what: &str| {
x509_parser::parse_x509_certificate(der)
.map(|(_, c)| {
(
c.issuer().as_raw().to_vec(),
c.issuer().to_string(),
c.subject().as_raw().to_vec(),
c.subject().to_string(),
)
})
.map_err(|err| Error::Pki(format!("{what} is not a usable X.509 certificate: {err}")))
};
let (leaf_issuer, leaf_issuer_name, _, _) = parse(&cert_der, "the certificate")?;
let (_, _, ca_subject, ca_subject_name) = parse(&ca_der, "the issuer")?;
Ok(if leaf_issuer == ca_subject {
Ok(())
} else {
Err((leaf_issuer_name, ca_subject_name))
})
}
pub fn not_after_ms(der: &[u8]) -> Result<UnixMillis> {
let (_rest, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
Ok(cert.validity().not_after.timestamp() * 1000)
}
#[cfg(test)]
mod not_before_tests {
use super::*;
#[test]
fn a_certificate_minted_before_the_clock_stepped_back_is_already_valid() {
let (nb, na) = validity(1).unwrap();
let now = OffsetDateTime::now_utc();
assert!(
nb <= now - Duration::hours(23),
"not_before {nb} is not a day back from {now}"
);
assert!(
na >= now + Duration::hours(23),
"the lifetime is unchanged: {na}"
);
assert_eq!(NOT_BEFORE_SLACK, Duration::hours(24));
}
}