trifid/trifid-pki/src/cert.rs

505 lines
19 KiB
Rust

//! Manage Nebula PKI Certificates
//! This is pretty much a direct port of nebula/cert/cert.go
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::net::Ipv4Addr;
use std::ops::Add;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use ed25519_dalek::ed25519::pkcs8::spki::der::Encode;
use ipnet::{Ipv4Net};
use pem::Pem;
use quick_protobuf::{BytesReader, MessageRead, Writer};
use sha256::digest;
use x25519_dalek::{PublicKey, StaticSecret};
use crate::ca::NebulaCAPool;
use crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};
/// The length, in bytes, of public keys
pub const PUBLIC_KEY_LENGTH: i32 = 32;
/// The PEM banner for certificates
pub const CERT_BANNER: &str = "NEBULA CERTIFICATE";
/// The PEM banner for X25519 private keys
pub const X25519_PRIVATE_KEY_BANNER: &str = "NEBULA X25519 PRIVATE KEY";
/// The PEM banner for X25519 public keys
pub const X25519_PUBLIC_KEY_BANNER: &str = "NEBULA X25519 PUBLIC KEY";
/// The PEM banner for Ed25519 private keys
pub const ED25519_PRIVATE_KEY_BANNER: &str = "NEBULA ED25519 PRIVATE KEY";
/// The PEM banner for Ed25519 public keys
pub const ED25519_PUBLIC_KEY_BANNER: &str = "NEBULA ED25519 PUBLIC KEY";
/// A Nebula PKI certificate
pub struct NebulaCertificate {
/// The signed data of this certificate
pub details: NebulaCertificateDetails,
/// The Ed25519 signature of this certificate
pub signature: Vec<u8>
}
/// The signed details contained in a Nebula PKI certificate
pub struct NebulaCertificateDetails {
/// The name of the identity this certificate was issued for
pub name: String,
/// The IPv4 addresses issued to this node
pub ips: Vec<Ipv4Net>,
/// The IPv4 subnets this node is responsible for routing
pub subnets: Vec<Ipv4Net>,
/// The groups this node is a part of
pub groups: Vec<String>,
/// Certificate start date and time
pub not_before: SystemTime,
/// Certificate expiry date and time
pub not_after: SystemTime,
/// Public key
pub public_key: [u8; PUBLIC_KEY_LENGTH as usize],
/// Is this node a CA?
pub is_ca: bool,
/// SHA256 of issuer certificate. If blank, this cert is self-signed.
pub issuer: String
}
/// A list of errors that can occur parsing certificates
#[derive(Debug)]
pub enum CertificateError {
/// Attempted to deserialize a certificate from an empty byte array
EmptyByteArray,
/// The encoded Details field is null
NilDetails,
/// The encoded Ips field is not formatted correctly
IpsNotPairs,
/// The encoded Subnets field is not formatted correctly
SubnetsNotPairs,
/// Signatures are expected to be 64 bytes but the signature on the certificate was a different length
WrongSigLength,
/// Public keys are expected to be 32 bytes but the public key on this cert is not
WrongKeyLength,
/// Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not
WrongPemTag,
/// This certificate either is not yet valid or has already expired
Expired,
/// The public key does not match the expected value
KeyMismatch
}
impl Display for CertificateError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyByteArray => write!(f, "Certificate bytearray is empty"),
Self::NilDetails => write!(f, "The encoded Details field is null"),
Self::IpsNotPairs => write!(f, "encoded IPs should be in pairs, an odd number was found"),
Self::SubnetsNotPairs => write!(f, "encoded subnets should be in pairs, an odd number was found"),
Self::WrongSigLength => write!(f, "Signature should be 64 bytes but is a different size"),
Self::WrongKeyLength => write!(f, "Public keys are expected to be 32 bytes but the public key on this cert is not"),
Self::WrongPemTag => write!(f, "Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not"),
Self::Expired => write!(f, "This certificate either is not yet valid or has already expired"),
Self::KeyMismatch => write!(f, "Key does not match expected value")
}
}
}
impl Error for CertificateError {}
fn map_cidr_pairs(pairs: &[u32]) -> Result<Vec<Ipv4Net>, Box<dyn Error>> {
let mut res_vec = vec![];
for pair in pairs.chunks(2) {
res_vec.push(Ipv4Net::with_netmask(Ipv4Addr::from(pair[0]), Ipv4Addr::from(pair[1]))?);
}
Ok(res_vec)
}
/// Given a protobuf-encoded certificate bytearray, deserialize it into a `NebulaCertificate` object.
/// # Errors
/// This function will return an error if there is a protobuf parsing error, or if the certificate data is invalid.
/// # Panics
pub fn deserialize_nebula_certificate(bytes: &[u8]) -> Result<NebulaCertificate, Box<dyn Error>> {
if bytes.is_empty() {
return Err(CertificateError::EmptyByteArray.into())
}
let mut reader = BytesReader::from_bytes(bytes);
let raw_cert = RawNebulaCertificate::from_reader(&mut reader, bytes)?;
let details = raw_cert.Details.ok_or(CertificateError::NilDetails)?;
if details.Ips.len() % 2 != 0 {
return Err(CertificateError::IpsNotPairs.into())
}
if details.Subnets.len() % 2 != 0 {
return Err(CertificateError::SubnetsNotPairs.into())
}
let mut nebula_cert;
#[allow(clippy::cast_sign_loss)]
{
nebula_cert = NebulaCertificate {
details: NebulaCertificateDetails {
name: details.Name.to_string(),
ips: map_cidr_pairs(&details.Ips)?,
subnets: map_cidr_pairs(&details.Subnets)?,
groups: details.Groups.iter().map(std::string::ToString::to_string).collect(),
not_before: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotBefore as u64)),
not_after: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotAfter as u64)),
public_key: [0u8; 32],
is_ca: details.IsCA,
issuer: hex::encode(details.Issuer),
},
signature: vec![],
};
}
nebula_cert.signature = raw_cert.Signature;
if details.PublicKey.len() != 32 {
return Err(CertificateError::WrongKeyLength.into())
}
#[allow(clippy::unwrap_used)] { nebula_cert.details.public_key = details.PublicKey.try_into().unwrap(); }
Ok(nebula_cert)
}
/// A list of errors that can occur parsing keys
#[derive(Debug)]
pub enum KeyError {
/// Keys should have their associated PEM tags but this had the wrong one
WrongPemTag
}
impl Display for KeyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongPemTag => write!(f, "Keys should have their associated PEM tags but this had the wrong one")
}
}
}
impl Error for KeyError {}
/// Deserialize the first PEM block in the given byte array into a `NebulaCertificate`
/// # Errors
/// This function will return an error if the PEM data is invalid, or if there is an error parsing the certificate (see `deserialize_nebula_certificate`)
pub fn deserialize_nebula_certificate_from_pem(bytes: &[u8]) -> Result<NebulaCertificate, Box<dyn Error>> {
let pem = pem::parse(bytes)?;
if pem.tag != CERT_BANNER {
return Err(CertificateError::WrongPemTag.into())
}
deserialize_nebula_certificate(&pem.contents)
}
/// Simple helper to PEM encode an X25519 private key
pub fn serialize_x25519_private(bytes: &[u8]) -> Vec<u8> {
pem::encode(&Pem {
tag: X25519_PRIVATE_KEY_BANNER.to_string(),
contents: bytes.to_vec(),
}).as_bytes().to_vec()
}
/// Simple helper to PEM encode an X25519 public key
pub fn serialize_x25519_public(bytes: &[u8]) -> Vec<u8> {
pem::encode(&Pem {
tag: X25519_PUBLIC_KEY_BANNER.to_string(),
contents: bytes.to_vec(),
}).as_bytes().to_vec()
}
/// Attempt to deserialize a PEM encoded X25519 private key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_x25519_private(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
let pem = pem::parse(bytes)?;
if pem.tag != X25519_PRIVATE_KEY_BANNER {
return Err(KeyError::WrongPemTag.into())
}
Ok(pem.contents)
}
/// Attempt to deserialize a PEM encoded X25519 public key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_x25519_public(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
let pem = pem::parse(bytes)?;
if pem.tag != X25519_PUBLIC_KEY_BANNER {
return Err(KeyError::WrongPemTag.into())
}
Ok(pem.contents)
}
/// Simple helper to PEM encode an Ed25519 private key
pub fn serialize_ed25519_private(bytes: &[u8]) -> Vec<u8> {
pem::encode(&Pem {
tag: ED25519_PRIVATE_KEY_BANNER.to_string(),
contents: bytes.to_vec(),
}).as_bytes().to_vec()
}
/// Simple helper to PEM encode an Ed25519 public key
pub fn serialize_ed25519_public(bytes: &[u8]) -> Vec<u8> {
pem::encode(&Pem {
tag: ED25519_PUBLIC_KEY_BANNER.to_string(),
contents: bytes.to_vec(),
}).as_bytes().to_vec()
}
/// Attempt to deserialize a PEM encoded Ed25519 private key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_ed25519_private(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
let pem = pem::parse(bytes)?;
if pem.tag != ED25519_PRIVATE_KEY_BANNER {
return Err(KeyError::WrongPemTag.into())
}
Ok(pem.contents)
}
/// Attempt to deserialize a PEM encoded Ed25519 public key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_ed25519_public(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
let pem = pem::parse(bytes)?;
if pem.tag != ED25519_PUBLIC_KEY_BANNER {
return Err(KeyError::WrongPemTag.into())
}
Ok(pem.contents)
}
impl NebulaCertificate {
/// Sign a nebula certificate with the provided private key
/// # Errors
/// This function will return an error if the certificate could not be serialized or signed.
pub fn sign(&mut self, key: &SigningKey) -> Result<(), Box<dyn Error>> {
let mut out = Vec::new();
let mut writer = Writer::new(&mut out);
writer.write_message(&self.get_raw_details())?;
let sig = key.sign(&out).to_bytes();
self.signature = match sig.to_vec() {
Ok(v) => v,
Err(_) => return Err("signature error".into())
};
Ok(())
}
/// Verify the signature on a certificate with the provided public key
/// # Errors
/// This function will return an error if the certificate could not be serialized or the signature could not be checked.
pub fn check_signature(&self, key: &VerifyingKey) -> Result<bool, Box<dyn Error>> {
let mut out = Vec::new();
let mut writer = Writer::new(&mut out);
writer.write_message(&self.get_raw_details())?;
Ok(key.verify(&out, &Signature::try_from(&*self.signature)?).is_ok())
}
/// Returns true if the signature is too young or too old compared to the provided time
pub fn expired(&self, time: SystemTime) -> bool {
self.details.not_before > time || self.details.not_after < time
}
/// Verify will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)
/// # Errors
/// This function will return an error if there is an error parsing the cert or the CA pool.
pub fn verify(&self, time: SystemTime, ca_pool: &NebulaCAPool) -> Result<CertificateValidity, Box<dyn Error>> {
if ca_pool.is_blocklisted(self) {
return Ok(CertificateValidity::Blocklisted);
}
let Some(signer) = ca_pool.get_ca_for_cert(self)? else { return Ok(CertificateValidity::NotSignedByThisCAPool) };
if signer.expired(time) {
return Ok(CertificateValidity::RootCertExpired)
}
if self.expired(time) {
return Ok(CertificateValidity::CertExpired)
}
if !self.check_signature(&VerifyingKey::from_bytes(&signer.details.public_key)?)? {
return Ok(CertificateValidity::BadSignature)
}
Ok(self.check_root_constraints(signer))
}
/// Make sure that this certificate does not break any of the constraints set by the signing certificate
pub fn check_root_constraints(&self, signer: &Self) -> CertificateValidity {
// Make sure this cert doesn't expire after the signer
if signer.details.not_before < self.details.not_before {
return CertificateValidity::CertExpiresAfterSigner;
}
// Make sure this cert doesn't come into validity before the root
if signer.details.not_before > self.details.not_before {
return CertificateValidity::CertValidBeforeSigner;
}
// If the signer contains a limited set of groups, make sure this cert only has a subset of them
if !signer.details.groups.is_empty() {
for group in &self.details.groups {
if !signer.details.groups.contains(group) {
return CertificateValidity::GroupNotPresentOnSigner;
}
}
}
// If the signer contains a limited set of IP ranges, make sure the cert only contains a subset
if !signer.details.ips.is_empty() {
for ip in &self.details.ips {
if !net_match(*ip, &signer.details.ips) {
return CertificateValidity::IPNotPresentOnSigner;
}
}
}
// If the signer contains a limited set of subnets, make sure the cert only contains a subset
if !signer.details.subnets.is_empty() {
for subnet in &self.details.subnets {
if !net_match(*subnet, &signer.details.subnets) {
return CertificateValidity::SubnetNotPresentOnSigner;
}
}
}
CertificateValidity::Ok
}
#[allow(clippy::unwrap_used)]
/// Verify if the given private key corresponds to the public key used to sign this certificate
/// # Errors
/// This function will return an error if either keys are invalid.
/// # Panics
/// This function, while containing calls to unwrap, has proper bounds checking and will not panic.
pub fn verify_private_key(&self, key: &[u8]) -> Result<(), Box<dyn Error>> {
if self.details.is_ca {
// convert the keys
if key.len() != 64 {
return Err("key not 64-bytes long".into())
}
let actual_private_key: [u8; 32] = (&key[..32]).try_into().unwrap();
if PublicKey::from(&StaticSecret::from(actual_private_key)) != PublicKey::from(self.details.public_key) {
return Err(CertificateError::KeyMismatch.into());
}
return Ok(());
}
if key.len() != 32 {
return Err("key not 32-bytes long".into())
}
let pubkey = x25519_dalek::x25519(key.try_into().unwrap(), x25519_dalek::X25519_BASEPOINT_BYTES);
if pubkey != self.details.public_key {
return Err(CertificateError::KeyMismatch.into());
}
Ok(())
}
/// Get a protobuf-ready raw struct, ready for serialization
#[allow(clippy::expect_used)]
#[allow(clippy::cast_possible_wrap)]
/// # Panics
/// This function will panic if time went backwards, or if the certificate contains extremely invalid data.
pub fn get_raw_details(&self) -> RawNebulaCertificateDetails {
let mut raw = RawNebulaCertificateDetails {
Name: self.details.name.clone(),
Ips: vec![],
Subnets: vec![],
Groups: self.details.groups.iter().map(std::convert::Into::into).collect(),
NotBefore: self.details.not_before.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
NotAfter: self.details.not_after.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
PublicKey: self.details.public_key.into(),
IsCA: self.details.is_ca,
Issuer: hex::decode(&self.details.issuer).expect("Issuer was not a hex-encoded value"),
};
for ip_net in &self.details.ips {
raw.Ips.push(ip_net.addr().into());
raw.Ips.push(ip_net.netmask().into());
}
for subnet in &self.details.subnets {
raw.Subnets.push(subnet.addr().into());
raw.Subnets.push(subnet.netmask().into());
}
raw
}
/// Will serialize this cert into a protobuf byte array.
/// # Errors
/// This function will return an error if protobuf was unable to serialize the data.
pub fn serialize(&self) -> Result<Vec<u8>, Box<dyn Error>> {
let raw_cert = RawNebulaCertificate {
Details: Some(self.get_raw_details()),
Signature: self.signature.clone(),
};
let mut out = vec![];
let mut writer = Writer::new(&mut out);
writer.write_message(&raw_cert)?;
Ok(out)
}
/// Will serialize this cert into a PEM byte array.
/// # Errors
/// This function will return an error if protobuf was unable to serialize the data.
pub fn serialize_to_pem(&self) -> Result<Vec<u8>, Box<dyn Error>> {
let pbuf_bytes = self.serialize()?;
Ok(pem::encode(&Pem {
tag: CERT_BANNER.to_string(),
contents: pbuf_bytes,
}).as_bytes().to_vec())
}
/// Get the fingerprint of this certificate
/// # Errors
/// This functiom will return an error if protobuf was unable to serialize the cert.
pub fn sha256sum(&self) -> Result<String, Box<dyn Error>> {
let pbuf_bytes = self.serialize()?;
Ok(digest(&pbuf_bytes[..]))
}
}
/// A list of possible errors that can happen validating a certificate
pub enum CertificateValidity {
/// There are no issues with this certificate
Ok,
/// This cert has been blocklisted in the given CA pool
Blocklisted,
/// The certificate that signed this cert is expired
RootCertExpired,
/// This cert is expired
CertExpired,
/// This cert's signature is invalid
BadSignature,
/// This cert was not signed by any CAs in the CA pool
NotSignedByThisCAPool,
/// This cert expires after the signer's cert expires
CertExpiresAfterSigner,
/// This cert enters validity before the signer's cert does
CertValidBeforeSigner,
/// A group present on this certificate is not present on the signer's certificate
GroupNotPresentOnSigner,
/// An IP present on this certificate is not present on the signer's certificate
IPNotPresentOnSigner,
/// A subnet on this certificate is not present on the signer's certificate
SubnetNotPresentOnSigner
}
fn net_match(cert_ip: Ipv4Net, root_ips: &Vec<Ipv4Net>) -> bool {
for net in root_ips {
if net.contains(&cert_ip) {
return true;
}
}
false
}