//! 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, MessageWrite, Writer}; use sha2::Sha256; use x25519_dalek::{PublicKey, StaticSecret}; use crate::ca::NebulaCAPool; use crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails}; use sha2::Digest; /// 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 #[derive(Debug)] pub struct NebulaCertificate { /// The signed data of this certificate pub details: NebulaCertificateDetails, /// The Ed25519 signature of this certificate pub signature: Vec } /// The signed details contained in a Nebula PKI certificate #[derive(Debug)] 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, /// The IPv4 subnets this node is responsible for routing pub subnets: Vec, /// The groups this node is a part of pub groups: Vec, /// 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, Box> { 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) } impl Display for NebulaCertificate { #[allow(clippy::unwrap_used)] fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "NebulaCertificate {{")?; writeln!(f, " Details {{")?; writeln!(f, " Name: {}", self.details.name)?; writeln!(f, " Ips: {:?}", self.details.ips)?; writeln!(f, " Subnets: {:?}", self.details.subnets)?; writeln!(f, " Groups: {:?}", self.details.groups)?; writeln!(f, " Not before: {:?}", self.details.not_before)?; writeln!(f, " Not after: {:?}", self.details.not_after)?; writeln!(f, " Is CA: {}", self.details.is_ca)?; writeln!(f, " Issuer: {}", self.details.issuer)?; writeln!(f, " Public key: {}", hex::encode(self.details.public_key))?; writeln!(f, " }}")?; writeln!(f, " Fingerprint: {}", self.sha256sum().unwrap())?; writeln!(f, " Signature: {}", hex::encode(self.signature.clone()))?; writeln!(f, "}}") } } /// 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> { 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> { 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 { 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 { 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, Box> { 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, Box> { 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 { 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 { 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, Box> { 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, Box> { 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> { 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> { 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> { 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> { 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, Box> { 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); raw_cert.write_message(&mut writer)?; 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, Box> { 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> { let pbuf_bytes = self.serialize()?; let mut hasher = Sha256::new(); hasher.update(pbuf_bytes); Ok(hex::encode(hasher.finalize())) } } /// 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) -> bool { for net in root_ips { if net.contains(&cert_ip) { return true; } } false }