diff --git a/Cargo.lock b/Cargo.lock index 4d5d078..cbb2a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -154,6 +154,12 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + [[package]] name = "binascii" version = "0.1.4" @@ -166,6 +172,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.3" @@ -252,6 +267,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "const-oid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" + [[package]] name = "constant_time_eq" version = "0.2.4" @@ -270,7 +291,7 @@ dependencies = [ "hmac", "percent-encoding", "rand", - "sha2", + "sha2 0.10.6", "subtle", "time 0.3.17", "version_check", @@ -351,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -364,6 +385,34 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +dependencies = [ + "cfg-if", + "digest 0.10.6", + "fiat-crypto", + "packed_simd_2", + "platforms", + "subtle", + "zeroize", +] + [[package]] name = "cxx" version = "1.0.91" @@ -408,6 +457,16 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "devise" version = "0.3.1" @@ -441,13 +500,22 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer", + "block-buffer 0.10.3", "crypto-common", "subtle", ] @@ -478,6 +546,29 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" +[[package]] +name = "ed25519" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf420a7ec85d98495b0c34aa4a58ca117f982ffbece111aeb545160148d7010" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.0.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd577ba9d4bcab443cac60003d8fd32c638e7024a3ec92c200d7af5d2c397ed" +dependencies = [ + "curve25519-dalek 4.0.0-rc.1", + "ed25519", + "serde", + "sha2 0.10.6", + "zeroize", +] + [[package]] name = "either" version = "1.8.1" @@ -508,6 +599,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fiat-crypto" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a214f5bb88731d436478f3ae1f8a277b62124089ba9fb67f4f93fb100ef73c90" + [[package]] name = "figment" version = "0.10.8" @@ -661,6 +758,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -773,7 +881,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.6", ] [[package]] @@ -917,6 +1025,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" + [[package]] name = "itertools" version = "0.10.5" @@ -953,6 +1067,12 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -1011,7 +1131,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest", + "digest 0.10.6", ] [[package]] @@ -1223,6 +1343,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1300,6 +1430,15 @@ dependencies = [ "syn", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1318,12 +1457,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + [[package]] name = "png" version = "0.17.7" @@ -1382,6 +1537,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + [[package]] name = "quote" version = "1.0.23" @@ -1399,7 +1563,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1409,7 +1573,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -1418,7 +1591,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.8", ] [[package]] @@ -1436,7 +1609,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] @@ -1686,7 +1859,20 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] [[package]] @@ -1697,7 +1883,17 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", +] + +[[package]] +name = "sha256" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328169f167261957e83d82be47f9e36629e257c62308129033d7f7e7c173d180" +dependencies = [ + "hex", + "sha2 0.9.9", ] [[package]] @@ -1718,6 +1914,12 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" + [[package]] name = "slab" version = "0.4.7" @@ -1749,6 +1951,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sqlformat" version = "0.2.1" @@ -1809,7 +2021,7 @@ dependencies = [ "serde", "serde_json", "sha1", - "sha2", + "sha2 0.10.6", "smallvec", "sqlformat", "sqlx-rt", @@ -1832,7 +2044,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2", + "sha2 0.10.6", "sqlx-core", "sqlx-rt", "syn", @@ -1896,6 +2108,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -2128,7 +2352,7 @@ dependencies = [ "qrcodegen", "rand", "sha1", - "sha2", + "sha2 0.10.6", "url", "urlencoding", ] @@ -2227,6 +2451,15 @@ dependencies = [ [[package]] name = "trifid-pki" version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "hex", + "ipnet", + "pem", + "quick-protobuf", + "sha256", + "x25519-dalek", +] [[package]] name = "try-lock" @@ -2338,7 +2571,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ - "getrandom", + "getrandom 0.2.8", "rand", "uuid-macro-internal", ] @@ -2382,6 +2615,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -2623,8 +2862,40 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "x25519-dalek" +version = "2.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/trifid-pki/Cargo.toml b/trifid-pki/Cargo.toml index bfb2c5b..c66566f 100644 --- a/trifid-pki/Cargo.toml +++ b/trifid-pki/Cargo.toml @@ -7,3 +7,10 @@ description = "A rust implementation of the Nebula PKI system" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +pem = "1.1.1" +x25519-dalek = "2.0.0-pre.1" +ed25519-dalek = "2.0.0-pre.0" +ipnet = "2.7.1" +quick-protobuf = "0.8.1" +hex = "0.4.3" +sha256 = "1.1.2" \ No newline at end of file diff --git a/trifid-pki/src/ca.rs b/trifid-pki/src/ca.rs new file mode 100644 index 0000000..a977c34 --- /dev/null +++ b/trifid-pki/src/ca.rs @@ -0,0 +1,101 @@ +//! Structs to represent a pool of CA's and blacklisted certificates + +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use ed25519_dalek::VerifyingKey; +use crate::cert::{deserialize_nebula_certificate_from_pem, NebulaCertificate}; + +/// A pool of trusted CA certificates, and certificates that should be blocked. +/// This is equivalent to the `pki` section in a typical Nebula config.yml. +#[derive(Default)] +pub struct NebulaCAPool { + /// The list of CA root certificates that should be trusted. + pub cas: HashMap, + /// The list of blocklisted certificate fingerprints + pub cert_blocklist: Vec, + /// True if any of the member CAs certificates are expired. Must be handled. + pub expired: bool +} + +impl NebulaCAPool { + /// Create a new, blank CA pool + pub fn new() -> Self { + Self::default() + } + + /// Create a new CA pool from a set of PEM encoded CA certificates. + /// If any of the certificates are expired, the pool will **still be returned**, with the expired flag set. + /// This must be handled properly. + /// # Errors + /// This function will return an error if PEM data provided was invalid. + pub fn new_from_pem(bytes: &[u8]) -> Result> { + let pems = pem::parse_many(bytes)?; + + let mut pool = Self::new(); + + for cert in pems { + match pool.add_ca_certificate(pem::encode(&cert).as_bytes()) { + Ok(did_expire) => if did_expire { pool.expired = true }, + Err(e) => return Err(e) + } + } + + Ok(pool) + } + + /// Add a given CA certificate to the CA pool. If the certificate is expired, it will **still be added** - the return value will be `true` instead of `false` + /// # Errors + /// This function will return an error if the certificate is invalid in any way. + pub fn add_ca_certificate(&mut self, bytes: &[u8]) -> Result> { + let cert = deserialize_nebula_certificate_from_pem(bytes)?; + + if !cert.details.is_ca { + return Err(CaPoolError::NotACA.into()) + } + + if !cert.check_signature(&VerifyingKey::from_bytes(&cert.details.public_key)?)? { + + } + + Ok(false) + } + + /// Checks if the given certificate is blocklisted + pub fn is_blocklisted(&self, cert: &NebulaCertificate) -> bool { + let Ok(h) = cert.sha256sum() else { return false }; + self.cert_blocklist.contains(&h) + } + + /// Gets the CA certificate used to sign the given certificate + /// # Errors + /// This function will return an error if the certificate does not have an issuer attached (it is self-signed) + pub fn get_ca_for_cert(&self, cert: &NebulaCertificate) -> Result, Box> { + if cert.details.issuer == String::new() { + return Err(CaPoolError::NoIssuer.into()) + } + + Ok(self.cas.get(&cert.details.issuer)) + } +} + +#[derive(Debug)] +/// A list of errors that can happen when working with a CA Pool +pub enum CaPoolError { + /// Tried to add a non-CA cert to the CA pool + NotACA, + /// Tried to add a non-self-signed cert to the CA pool (all CAs must be root certificates) + NotSelfSigned, + /// Tried to look up a certificate that does not have an issuer field + NoIssuer +} +impl Error for CaPoolError {} +impl Display for CaPoolError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotACA => write!(f, "Tried to add a non-CA cert to the CA pool"), + Self::NotSelfSigned => write!(f, "Tried to add a non-self-signed cert to the CA pool (all CAs must be root certificates)"), + Self::NoIssuer => write!(f, "Tried to look up a certificate with a null issuer field") + } + } +} \ No newline at end of file diff --git a/trifid-pki/src/cert.rs b/trifid-pki/src/cert.rs new file mode 100644 index 0000000..ee164bc --- /dev/null +++ b/trifid-pki/src/cert.rs @@ -0,0 +1,505 @@ +//! 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 +} + +/// 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, + /// 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) +} + +/// 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); + 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, 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()?; + + 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) -> bool { + for net in root_ips { + if net.contains(&cert_ip) { + return true; + } + } + false +} \ No newline at end of file diff --git a/trifid-pki/src/cert_codec.proto b/trifid-pki/src/cert_codec.proto new file mode 100644 index 0000000..87fe15f --- /dev/null +++ b/trifid-pki/src/cert_codec.proto @@ -0,0 +1,27 @@ +// Copied from https://github.com/slackhq/nebula/blob/master/cert/cert.proto +// with some minimal changes for trifid-pki. +syntax = "proto3"; +package cert_codec; + +message RawNebulaCertificate { + RawNebulaCertificateDetails Details = 1; + bytes Signature = 2; +} + +message RawNebulaCertificateDetails { + string Name = 1; + + // Ips and Subnets are in big endian 32 bit pairs, 1st the ip, 2nd the mask + repeated uint32 Ips = 2; + repeated uint32 Subnets = 3; + + repeated string Groups = 4; + int64 NotBefore = 5; + int64 NotAfter = 6; + bytes PublicKey = 7; + + bool IsCA = 8; + + // sha-256 of the issuer certificate, if this field is blank the cert is self-signed + bytes Issuer = 9; +} \ No newline at end of file diff --git a/trifid-pki/src/cert_codec.rs b/trifid-pki/src/cert_codec.rs new file mode 100644 index 0000000..7710326 --- /dev/null +++ b/trifid-pki/src/cert_codec.rs @@ -0,0 +1,116 @@ +// Automatically generated rust module for 'cert_codec.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(clippy::wildcard_imports)] + +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RawNebulaCertificate { + pub Details: Option, + pub Signature: Vec, +} + +impl<'a> MessageRead<'a> for RawNebulaCertificate { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.Details = Some(r.read_message::(bytes)?), + Ok(18) => msg.Signature = r.read_bytes(bytes)?.to_owned(), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for RawNebulaCertificate { + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.Details { w.write_with_tag(10, |w| w.write_message(s))?; } + if !self.Signature.is_empty() { w.write_with_tag(18, |w| w.write_bytes(&**&self.Signature))?; } + Ok(()) + } + + fn get_size(&self) -> usize { + 0 + + self.Details.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + + if self.Signature.is_empty() { 0 } else { 1 + sizeof_len((&self.Signature).len()) } + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RawNebulaCertificateDetails { + pub Name: String, + pub Ips: Vec, + pub Subnets: Vec, + pub Groups: Vec, + pub NotBefore: i64, + pub NotAfter: i64, + pub PublicKey: Vec, + pub IsCA: bool, + pub Issuer: Vec, +} + +impl<'a> MessageRead<'a> for RawNebulaCertificateDetails { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.Name = r.read_string(bytes)?.to_owned(), + Ok(18) => msg.Ips = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?, + Ok(26) => msg.Subnets = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?, + Ok(34) => msg.Groups.push(r.read_string(bytes)?.to_owned()), + Ok(40) => msg.NotBefore = r.read_int64(bytes)?, + Ok(48) => msg.NotAfter = r.read_int64(bytes)?, + Ok(58) => msg.PublicKey = r.read_bytes(bytes)?.to_owned(), + Ok(64) => msg.IsCA = r.read_bool(bytes)?, + Ok(74) => msg.Issuer = r.read_bytes(bytes)?.to_owned(), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +#[allow(clippy::cast_sign_loss)] +impl MessageWrite for RawNebulaCertificateDetails { + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.Name != String::default() { w.write_with_tag(10, |w| w.write_string(&**&self.Name))?; } + w.write_packed_with_tag(18, &self.Ips, |w, m| w.write_uint32(*m), &|m| sizeof_varint(u64::from(*(m))))?; + w.write_packed_with_tag(26, &self.Subnets, |w, m| w.write_uint32(*m), &|m| sizeof_varint(u64::from(*(m))))?; + for s in &self.Groups { w.write_with_tag(34, |w| w.write_string(&**s))?; } + if self.NotBefore != 0i64 { w.write_with_tag(40, |w| w.write_int64(*&self.NotBefore))?; } + if self.NotAfter != 0i64 { w.write_with_tag(48, |w| w.write_int64(*&self.NotAfter))?; } + if !self.PublicKey.is_empty() { w.write_with_tag(58, |w| w.write_bytes(&**&self.PublicKey))?; } + if self.IsCA != false { w.write_with_tag(64, |w| w.write_bool(*&self.IsCA))?; } + if !self.Issuer.is_empty() { w.write_with_tag(74, |w| w.write_bytes(&**&self.Issuer))?; } + Ok(()) + } + + fn get_size(&self) -> usize { + 0 + + if self.Name == String::default() { 0 } else { 1 + sizeof_len((&self.Name).len()) } + + if self.Ips.is_empty() { 0 } else { 1 + sizeof_len(self.Ips.iter().map(|s| sizeof_varint(u64::from(*(s)))).sum::()) } + + if self.Subnets.is_empty() { 0 } else { 1 + sizeof_len(self.Subnets.iter().map(|s| sizeof_varint(u64::from(*(s)))).sum::()) } + + self.Groups.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + + if self.NotBefore == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.NotBefore) as u64) } + + if self.NotAfter == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.NotAfter) as u64) } + + if self.PublicKey.is_empty() { 0 } else { 1 + sizeof_len((&self.PublicKey).len()) } + + if self.IsCA == false { 0 } else { 1 + sizeof_varint(u64::from(*(&self.IsCA))) } + + if self.Issuer.is_empty() { 0 } else { 1 + sizeof_len((&self.Issuer).len()) } + } +} + diff --git a/trifid-pki/src/lib.rs b/trifid-pki/src/lib.rs index 7d12d9a..2a2454c 100644 --- a/trifid-pki/src/lib.rs +++ b/trifid-pki/src/lib.rs @@ -1,14 +1,25 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +//! # trifid-pki +//! trifid-pki is a crate for interacting with the Nebula PKI system. It was created to prevent the need to make constant CLI calls for signing operations in Nebula. +//! It is designed to be interoperable with the original Go implementation and as such has some oddities with key management to ensure compatability. +//! +//! This crate has not received any format security audits, however the underlying crates used for actual cryptographic operations (ed25519-dalek and curve25519-dalek) have been audited with no major issues. +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(missing_docs)] +#![deny(clippy::missing_errors_doc)] +#![deny(clippy::missing_panics_doc)] +#![deny(clippy::missing_safety_doc)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::module_name_repetitions)] + + +pub mod ca; +pub mod cert; +pub(crate) mod cert_codec; #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[macro_use] +pub mod test; \ No newline at end of file diff --git a/trifid-pki/src/test.rs b/trifid-pki/src/test.rs new file mode 100644 index 0000000..53b5063 --- /dev/null +++ b/trifid-pki/src/test.rs @@ -0,0 +1,60 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use crate::netmask; +use std::net::Ipv4Addr; +use std::time::{Duration, SystemTime}; +use ipnet::Ipv4Net; +use crate::cert::{deserialize_nebula_certificate, NebulaCertificate, NebulaCertificateDetails}; +use std::str::FromStr; + +#[test] +fn certificate_serialization() { + let before = SystemTime::now() - Duration::from_secs(60); + let after = SystemTime::now() + Duration::from_secs(60); + let pub_key = b"1234567890abcedfghij1234567890ab"; + + let cert = NebulaCertificate { + details: NebulaCertificateDetails { + name: "testing".to_string(), + ips: vec![ + netmask!("10.1.1.1", "255.255.255.0"), + netmask!("10.1.1.2", "255.255.0.0"), + netmask!("10.1.1.3", "255.0.255.0") + ], + subnets: vec![ + netmask!("9.1.1.1", "255.0.255.0"), + netmask!("9.1.1.2", "255.255.255.0"), + netmask!("9.1.1.3", "255.255.0.0") + ], + groups: vec!["test-group1".to_string(), "test-group2".to_string(), "test-group3".to_string()], + not_before: before, + not_after: after, + public_key: *pub_key, + is_ca: false, + issuer: "1234567890abcedfghij1234567890ab".to_string(), + }, + signature: hex::decode("1234567890abcedfghij1234567890ab").unwrap(), + }; + + let bytes = cert.serialize().unwrap(); + + let deserialized = deserialize_nebula_certificate(&bytes).unwrap(); +/* +assert.Equal(t, nc.Details.Name, nc2.Details.Name) + assert.Equal(t, nc.Details.NotBefore, nc2.Details.NotBefore) + assert.Equal(t, nc.Details.NotAfter, nc2.Details.NotAfter) + assert.Equal(t, nc.Details.PublicKey, nc2.Details.PublicKey) + assert.Equal(t, nc.Details.IsCA, nc2.Details.IsCA) + */ + assert_eq!(cert.signature, deserialized.signature); + assert_eq!(cert.details.name, deserialized.details.name); + assert_eq!(cert.details.not_before, deserialized.details.not_after); +} + +#[macro_export] +macro_rules! netmask { + ($ip:expr,$mask:expr) => { + Ipv4Net::with_netmask(Ipv4Addr::from_str($ip).unwrap(), Ipv4Addr::from_str($mask).unwrap()).unwrap() + }; +} \ No newline at end of file