commit 171e9f3a20cf09a08b9cdb11bf9e317832af1a93 Author: c0repwn3r Date: Tue May 2 12:51:48 2023 -0400 E3PF PKI diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/e3pf.iml b/.idea/e3pf.iml new file mode 100644 index 0000000..7939757 --- /dev/null +++ b/.idea/e3pf.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3ce3588 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9be3db2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..23e7230 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,149 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "libepf" +version = "0.1.0" +dependencies = [ + "hex", + "pem", + "rmp-serde", + "serde", + "serde_arrays", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pem" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7f6097 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "libepf" +] \ No newline at end of file diff --git a/libepf/Cargo.lock b/libepf/Cargo.lock new file mode 100644 index 0000000..61d494f --- /dev/null +++ b/libepf/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "e3pf" +version = "0.1.0" diff --git a/libepf/Cargo.toml b/libepf/Cargo.toml new file mode 100644 index 0000000..2aa1016 --- /dev/null +++ b/libepf/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "libepf" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_arrays = "0.1" # TODO: Remove. Pending on serde supporting const generics +rmp-serde = "1" +pem = "2" +hex = "0.4" \ No newline at end of file diff --git a/libepf/src/lib.rs b/libepf/src/lib.rs new file mode 100644 index 0000000..aa1c855 --- /dev/null +++ b/libepf/src/lib.rs @@ -0,0 +1 @@ +pub mod pki; \ No newline at end of file diff --git a/libepf/src/pki.rs b/libepf/src/pki.rs new file mode 100644 index 0000000..c967d50 --- /dev/null +++ b/libepf/src/pki.rs @@ -0,0 +1,253 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use pem::Pem; +use serde::{Deserialize, Serialize}; + +pub const EPFPKI_PUBLIC_KEY_LENGTH: usize = 32; +pub const EPFPKI_SIGNATURE_LENGTH: usize = 64; + +pub type EpfPublicKey = [u8; 32]; +pub type EpfPrivateKey = [u8; 64]; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] +pub struct EPFCertificate { + pub details: EPFCertificateDetails, + pub fingerprint: String, + #[serde(with = "serde_arrays")] + pub signature: [u8; EPFPKI_SIGNATURE_LENGTH] +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] +pub struct EPFCertificateDetails { + pub name: String, + + pub not_before: u64, + pub not_after: u64, + + pub public_key: [u8; EPFPKI_PUBLIC_KEY_LENGTH], + + pub issuer_name: String, + pub issuer_fingerprint: String, + + pub claims: HashMap +} + +pub trait EpfPkiSerializable { + const PEM_BANNER: &'static str; + + fn as_bytes(&self) -> Result, rmp_serde::encode::Error>; + fn from_bytes(bytes: &[u8]) -> Result where Self: Sized; + + fn as_pem(&self) -> Result, Box>; + fn from_pem(bytes: &[u8]) -> Result> where Self: Sized; +} + +#[cfg_attr(tarpaulin, ignore)] +impl Display for EPFCertificate { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "EPFCertificate {{")?; + writeln!(f, "\tDetails: {{")?; + writeln!(f, "\t\tName: {}", self.details.name)?; + writeln!(f, "\t\tNot Before: {}", self.details.not_before)?; + writeln!(f, "\t\tNot After: {}", self.details.not_after)?; + writeln!(f, "\t\tPublic Key: {}", hex::encode(self.details.public_key))?; + writeln!(f, "\t\tIssuer: {{")?; + writeln!(f, "\t\t\tName: {}", self.details.issuer_name)?; + writeln!(f, "\t\t\tFingerprint: {}", self.details.issuer_fingerprint)?; + writeln!(f, "\t\t}}")?; + writeln!(f, "\t}}")?; + writeln!(f, "\tFingerprint: {}", self.fingerprint)?; + writeln!(f, "\tSignature: {}", hex::encode(self.signature))?; + writeln!(f, "}}") + } +} + +impl EpfPkiSerializable for EPFCertificate { + const PEM_BANNER: &'static str = "EPF CERTIFICATE"; + + fn as_bytes(&self) -> Result, rmp_serde::encode::Error> { + rmp_serde::to_vec(self) + } + + fn from_bytes(bytes: &[u8]) -> Result { + rmp_serde::from_slice(bytes) + } + + fn as_pem(&self) -> Result, Box> { + Ok(pem::encode(&Pem::new(Self::PEM_BANNER, self.as_bytes()?)).as_bytes().to_vec()) + } + + fn from_pem(bytes: &[u8]) -> Result> where Self: Sized { + let pem = pem::parse(bytes)?; + if pem.tag() != Self::PEM_BANNER { + return Err("Not a certificate".into()) + } + Ok(Self::from_bytes(pem.contents())?) + } +} + +impl EpfPkiSerializable for EpfPublicKey { + const PEM_BANNER: &'static str = "EPF PUBLIC KEY"; + + fn as_bytes(&self) -> Result, rmp_serde::encode::Error> { + Ok(self.to_vec()) + } + + fn from_bytes(bytes: &[u8]) -> Result { + bytes.try_into().map_err(|_| rmp_serde::decode::Error::LengthMismatch(bytes.len() as u32)) + } + + fn as_pem(&self) -> Result, Box> { + Ok(pem::encode(&Pem::new(Self::PEM_BANNER, self.as_bytes()?)).as_bytes().to_vec()) + } + + fn from_pem(bytes: &[u8]) -> Result> where Self: Sized { + let pem = pem::parse(bytes)?; + if pem.tag() != Self::PEM_BANNER { + return Err("Not a public key".into()) + } + Ok(Self::from_bytes(pem.contents())?) + } +} + +impl EpfPkiSerializable for EpfPrivateKey { + const PEM_BANNER: &'static str = "EPF PRIVATE KEY"; + + fn as_bytes(&self) -> Result, rmp_serde::encode::Error> { + Ok(self.to_vec()) + } + + fn from_bytes(bytes: &[u8]) -> Result { + bytes.try_into().map_err(|_| rmp_serde::decode::Error::LengthMismatch(bytes.len() as u32)) + } + + fn as_pem(&self) -> Result, Box> { + Ok(pem::encode(&Pem::new(Self::PEM_BANNER, self.as_bytes()?)).as_bytes().to_vec()) + } + + fn from_pem(bytes: &[u8]) -> Result> where Self: Sized { + let pem = pem::parse(bytes)?; + if pem.tag() != Self::PEM_BANNER { + return Err("Incorrect PEM tag".into()) + } + Ok(Self::from_bytes(pem.contents())?) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use crate::pki::{EPFCertificate, EPFCertificateDetails, EPFPKI_PUBLIC_KEY_LENGTH, EPFPKI_SIGNATURE_LENGTH, EpfPkiSerializable, EpfPrivateKey, EpfPublicKey}; + + #[test] + pub fn certificate_serialization() { + assert_eq!(cert().as_bytes().unwrap(), cert_bytes()) + } + + #[test] + pub fn certificate_deserialization() { + assert_eq!(EPFCertificate::from_bytes(&cert_bytes()).unwrap(), cert()) + } + + #[test] + pub fn certificate_serialization_pem() { + assert_eq!(cert().as_pem().unwrap(), cert_pem()) + } + + #[test] + pub fn certificate_deserialization_pem() { + assert_eq!(EPFCertificate::from_pem(&cert_pem()).unwrap(), cert()) + } + + #[test] + #[should_panic] + pub fn certificate_deserialization_pem_wrong_tag() { + EPFCertificate::from_pem(&null_public_key_pem()).unwrap(); + } + + #[test] + pub fn pubkey_serialization() { + assert_eq!(([0u8; 32]).as_bytes().unwrap(), [0u8; 32].to_vec()) + } + + #[test] + pub fn pubkey_deserialization() { + assert_eq!(EpfPublicKey::from_bytes(&[0u8; 32]).unwrap(), [0u8; 32]) + } + + #[test] + pub fn pubkey_serialization_pem() { + assert_eq!(([0u8; 32]).as_pem().unwrap(), null_public_key_pem()) + } + + #[test] + pub fn pubkey_deserialization_pem() { + assert_eq!(EpfPublicKey::from_pem(&null_public_key_pem()).unwrap(), [0u8; 32]) + } + + #[test] + #[should_panic] + pub fn pubkey_deserialization_pem_wrong_tag() { + EpfPublicKey::from_pem(&null_private_key_pem()).unwrap(); + } + + #[test] + pub fn privkey_serialization() { + assert_eq!(([0u8; 64]).as_bytes().unwrap(), [0u8; 64].to_vec()) + } + + #[test] + pub fn privkey_deserialization() { + assert_eq!(EpfPrivateKey::from_bytes(&[0u8; 64]).unwrap(), [0u8; 64]) + } + + #[test] + pub fn privkey_serialization_pem() { + assert_eq!(([0u8; 64]).as_pem().unwrap(), null_private_key_pem()) + } + + #[test] + pub fn privkey_deserialization_pem() { + assert_eq!(EpfPrivateKey::from_pem(&null_private_key_pem()).unwrap(), [0u8; 64]) + } + + #[test] + pub fn cert_display() { + println!("{}", cert()); + } + + #[test] + #[should_panic] + pub fn privkey_deserialization_pem_wrong_tag() { + EPFCertificate::from_pem(&null_public_key_pem()).unwrap(); + } + + fn cert() -> EPFCertificate { + EPFCertificate { + details: EPFCertificateDetails { + name: "Invalid Testing Certificate".to_string(), + not_before: 0, + not_after: 0, + public_key: [0u8; EPFPKI_PUBLIC_KEY_LENGTH], + issuer_name: "Invalid Testing Certificate Issuer".to_string(), + issuer_fingerprint: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + claims: HashMap::new() + }, + fingerprint: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + signature: [0u8; EPFPKI_SIGNATURE_LENGTH], + } + } + fn cert_bytes() -> Vec { + vec![147, 151, 187, 73, 110, 118, 97, 108, 105, 100, 32, 84, 101, 115, 116, 105, 110, 103, 32, 67, 101, 114, 116, 105, 102, 105, 99, 97, 116, 101, 0, 0, 220, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 217, 34, 73, 110, 118, 97, 108, 105, 100, 32, 84, 101, 115, 116, 105, 110, 103, 32, 67, 101, 114, 116, 105, 102, 105, 99, 97, 116, 101, 32, 73, 115, 115, 117, 101, 114, 217, 64, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 128, 217, 64, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 220, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + } + fn cert_pem() -> Vec { + vec![45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 69, 80, 70, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 13, 10, 107, 53, 101, 55, 83, 87, 53, 50, 89, 87, 120, 112, 90, 67, 66, 85, 90, 88, 78, 48, 97, 87, 53, 110, 73, 69, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 65, 65, 68, 99, 65, 67, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 13, 10, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 78, 107, 105, 83, 87, 53, 50, 89, 87, 120, 112, 90, 67, 66, 85, 90, 88, 78, 48, 97, 87, 53, 110, 73, 69, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 13, 10, 73, 69, 108, 122, 99, 51, 86, 108, 99, 116, 108, 65, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 13, 10, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 73, 68, 90, 81, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 13, 10, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 65, 119, 77, 68, 68, 99, 65, 69, 65, 65, 13, 10, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 13, 10, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 13, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 69, 80, 70, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 13, 10] + } + fn null_public_key_pem() -> Vec { + vec![45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 69, 80, 70, 32, 80, 85, 66, 76, 73, 67, 32, 75, 69, 89, 45, 45, 45, 45, 45, 13, 10, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 61, 13, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 69, 80, 70, 32, 80, 85, 66, 76, 73, 67, 32, 75, 69, 89, 45, 45, 45, 45, 45, 13, 10] + } + fn null_private_key_pem() -> Vec { + vec![45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 69, 80, 70, 32, 80, 82, 73, 86, 65, 84, 69, 32, 75, 69, 89, 45, 45, 45, 45, 45, 13, 10, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 13, 10, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 61, 61, 13, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 69, 80, 70, 32, 80, 82, 73, 86, 65, 84, 69, 32, 75, 69, 89, 45, 45, 45, 45, 45, 13, 10] + } +} \ No newline at end of file diff --git a/tarpaulin-report.html b/tarpaulin-report.html new file mode 100644 index 0000000..dbd85a4 --- /dev/null +++ b/tarpaulin-report.html @@ -0,0 +1,660 @@ + + + + + + + +
+ + + + + + \ No newline at end of file