2023-02-27 02:58:45 +00:00
//! 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 ipnet ::{ Ipv4Net } ;
use pem ::Pem ;
2023-02-27 15:04:10 +00:00
use quick_protobuf ::{ BytesReader , MessageRead , MessageWrite , Writer } ;
use sha2 ::Sha256 ;
2023-02-27 02:58:45 +00:00
use x25519_dalek ::{ PublicKey , StaticSecret } ;
use crate ::ca ::NebulaCAPool ;
use crate ::cert_codec ::{ RawNebulaCertificate , RawNebulaCertificateDetails } ;
2023-02-27 15:04:10 +00:00
use sha2 ::Digest ;
2023-02-27 02:58:45 +00:00
/// 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
2023-02-27 15:04:10 +00:00
#[ derive(Debug) ]
2023-02-27 02:58:45 +00:00
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
2023-02-27 15:04:10 +00:00
#[ derive(Debug) ]
2023-02-27 02:58:45 +00:00
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 )
}
2023-02-27 15:04:10 +00:00
impl Display for NebulaCertificate {
2023-02-27 15:24:30 +00:00
#[ allow(clippy::unwrap_used) ]
2023-02-27 15:04:10 +00:00
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 , " }} " )
}
}
2023-02-27 02:58:45 +00:00
/// 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 : [ 0 u8 ; 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 ) ;
2023-02-27 15:55:53 +00:00
self . get_raw_details ( ) . write_message ( & mut writer ) ? ;
2023-02-27 02:58:45 +00:00
2023-02-27 15:55:53 +00:00
self . signature = key . sign ( & out ) . to_vec ( ) ;
2023-02-27 02:58:45 +00:00
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 ) ;
2023-02-27 15:55:53 +00:00
self . get_raw_details ( ) . write_message ( & mut writer ) ? ;
let sig = Signature ::from_slice ( & self . signature ) ? ;
2023-02-27 02:58:45 +00:00
2023-02-27 15:55:53 +00:00
Ok ( key . verify ( & out , & sig ) . is_ok ( ) )
2023-02-27 02:58:45 +00:00
}
/// 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 ) ;
2023-02-27 15:04:10 +00:00
raw_cert . write_message ( & mut writer ) ? ;
2023-02-27 02:58:45 +00:00
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 ( ) ? ;
2023-02-27 15:04:10 +00:00
let mut hasher = Sha256 ::new ( ) ;
hasher . update ( pbuf_bytes ) ;
Ok ( hex ::encode ( hasher . finalize ( ) ) )
2023-02-27 02:58:45 +00:00
}
}
/// 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
}