diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 22fdd5c..e3f028d 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,7 +4,7 @@ on: push: branches: ["main"] pull_request: - branches: ["main", "dev"] + branches: ["*"] env: CARGO_TERM_COLOR: always @@ -30,25 +30,23 @@ jobs: cargo build --verbose --all-features cargo test --verbose --all-features --tests --examples fi - wasm: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - cache-all-crates: "true" - prefix-key: "macos" - - name: Run WASM tests with Safari, Firefox, Chrome - run: | - rustup target add wasm32-unknown-unknown - curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash - cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force - cargo binstall --no-confirm wasm-pack --force - wasm-pack test --node -- --features wasm --examples - wasm-pack test --firefox -- --features wasm --examples - wasm-pack test --chrome -- --features wasm --examples - wasm-pack test --safari -- --features wasm --examples - wasm-pack test --node -- --features wasm - wasm-pack test --firefox -- --features wasm - wasm-pack test --chrome -- --features wasm - wasm-pack test --safari -- --features wasm + # wasm: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: Swatinem/rust-cache@v2 + # with: + # cache-all-crates: "true" + # prefix-key: "macos" + # - name: Run WASM tests with Safari, Firefox, Chrome + # run: | + # rustup target add wasm32-unknown-unknown + # curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + # cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force + # cargo binstall --no-confirm wasm-pack --force + # wasm-pack test --node -- --features wasm --examples + # wasm-pack test --firefox -- --features wasm --examples + # wasm-pack test --chrome -- --features wasm --examples + # wasm-pack test --node -- --features wasm + # wasm-pack test --firefox -- --features wasm + # wasm-pack test --chrome -- --features wasm diff --git a/.gitignore b/.gitignore index 746c8a7..93ce6de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ .VSCodeCounter .coverage cert.csr -cert.der \ No newline at end of file +cert.der +/firedbg \ No newline at end of file diff --git a/.vscode/ltex.dictionary.en-US.txt b/.vscode/ltex.dictionary.en-US.txt index 9725610..42e4873 100644 --- a/.vscode/ltex.dictionary.en-US.txt +++ b/.vscode/ltex.dictionary.en-US.txt @@ -52,3 +52,4 @@ extnValue extnID CSRs IdCsrInner +TryFrom diff --git a/.vscode/ltex.hiddenFalsePositives.en-US.txt b/.vscode/ltex.hiddenFalsePositives.en-US.txt index eb4a05a..6616a09 100644 --- a/.vscode/ltex.hiddenFalsePositives.en-US.txt +++ b/.vscode/ltex.hiddenFalsePositives.en-US.txt @@ -15,3 +15,8 @@ {"rule":"UNLIKELY_OPENING_PUNCTUATION","sentence":"^\\Q:3\\E$"} {"rule":"DOUBLE_PUNCTUATION","sentence":"^\\QExtensions ::= SEQUENCE SIZE (1..MAX) OF Extension\\E$"} {"rule":"COMMA_PARENTHESIS_WHITESPACE","sentence":"^\\QExtension ::= SEQUENCE {\nextnID OBJECT IDENTIFIER,\ncritical BOOLEAN DEFAULT FALSE,\nextnValue OCTET STRING\n-- contains the DER encoding of an ASN.1 value\n-- corresponding to the extension type identified\n-- by extnID\n}\\E$"} +{"rule":"MORFOLOGIK_RULE_EN_US","sentence":"^\\QResponse counterpart of CreateSessionSchema.\\E$"} +{"rule":"MASS_AGREEMENT","sentence":"^\\QThe challenge string.\\E$"} +{"rule":"MORFOLOGIK_RULE_EN_US","sentence":"^\\QResponse counterpart of IdentifyRequest.\\E$"} +{"rule":"EN_A_VS_AN","sentence":"^\\QTry to convert the inner byte slice to a u128.\\E$"} +{"rule":"POSSESSIVE_APOSTROPHE","sentence":"^\\QView the examples directory for a simple example on how to implement and use this crate with the ED25519 signature algorithm.\\E$"} diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c6d107..9d4cb70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "markiscodecoverage.searchCriteria": ".coverage/lcov*.info" + "markiscodecoverage.searchCriteria": ".coverage/lcov*.info", + "rust-analyzer.cargo.features": ["types", "reqwest"] } diff --git a/Cargo.toml b/Cargo.toml index 4f2f05f..872bdb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,32 +1,48 @@ [package] name = "polyproto" -version = "0.9.0-alpha.1" +version = "0.9.0" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" repository = "https://github.com/polyphony-chat/polyproto" -rust-version = "1.65.0" +rust-version = "1.71.1" [lib] crate-type = ["rlib", "cdylib", "staticlib"] [features] +default = ["types"] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] +types = ["dep:http"] +reqwest = ["dep:reqwest", "types", "serde", "dep:url"] +serde = ["dep:serde", "dep:serde_json"] [dependencies] -der = { version = "0.7.8", features = ["pem"] } -getrandom = { version = "0.2.12", optional = true } +der = { version = "0.7.9", features = ["pem"] } +getrandom = { version = "0.2.14", optional = true } regex = "1.10.4" -spki = "0.7.3" -thiserror = "1.0.57" -x509-cert = { version = "0.2.5", default-features = false } +reqwest = { version = "0.12.4", features = ["json"], optional = true } +serde = { version = "1.0.199", optional = true, features = ["derive"] } +serde_json = { version = "1.0.116", optional = true } +spki = { version = "0.7.3", features = ["pem"] } +thiserror = "1.0.59" +x509-cert = "0.2.5" +log = "0.4.21" +url = { version = "2.5.0", optional = true } +http = { version = "1.1.0", optional = true } [dev-dependencies] ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } +env_logger = "0.11.3" +httptest = "0.16.1" rand = "0.8.5" -polyproto = { path = "./" } +tokio = { version = "1.37.0", features = ["full"] } +serde = { version = "1.0.199", features = ["derive"] } +serde_json = { version = "1.0.116" } +serde_test = "1.0.176" +polyproto = { path = "./", features = ["types", "reqwest", "serde"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.39" -wasm-bindgen = "0.2.89" +wasm-bindgen-test = "0.3.42" +wasm-bindgen = "0.2.92" diff --git a/README.md b/README.md index d028e97..456b496 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,18 @@ Crate supplying (generic) Rust types and traits to quickly get a [polyproto](https://docs.polyphony.chat/Protocol%20Specifications/core/) implementation up and -running. +running, as well as an HTTP client for the polyproto API. -## Implementing polyproto +Building upon types offered by the [der](https://crates.io/crates/der), +[x509_cert](https://crates.io/crates/x509_cert) and [spki](https://crates.io/crates/spki) crates, +this crate provides a set of types and traits to quickly implement the polyproto specification. +Simply add cryptography and signature algorithm crates of your choice to the mix, and you are ready +to go. -**The crate is currently in an alpha stage. Some functionality is missing, and -things may break or change at any point in time.** +All polyproto certificate types can be converted to and from the types offered by the `x509_cert` +crate. -This crate extends upon types offered by [der](https://crates.io/crates/der) and -[spki](https://crates.io/crates/spki). As such, these crates are required dependencies for -projects looking to implement polyproto. +## Implementing polyproto Start by implementing the trait [crate::signature::Signature] for a signature algorithm of your choice. Popular crates for cryptography and signature algorithms supply their own `PublicKey` and @@ -31,8 +33,8 @@ choice. Popular crates for cryptography and signature algorithms supply their ow You can then use the [crate::certs] types to build certificates using your implementations of the aforementioned traits. -View the [examples](./examples/) directory for a simple example on how to implement and use this -crate. +**View the [examples](./examples/)** directory for a simple example on how to implement and use this +crate with the ED25519 signature algorithm. ## Cryptography @@ -41,6 +43,15 @@ implementing polyproto by transforming the [polyproto specification](https://docs.polyphony.chat/Protocol%20Specifications/core/) into well-defined yet adaptable Rust types. +## Safety + +Please refer to the documentation of individual functions for information on which safety guarantees +they provide. Methods returning certificates, certificate requests and other types where the +validity and correctness of the data has a chance of impacting the security of a system always +mention the safety guarantees they provide in their respective documentation. + +This crate has not undergone any security audits. + ## WebAssembly This crate is designed to work with the `wasm32-unknown-unknown` target. To compile for `wasm`, you @@ -51,6 +62,18 @@ will have to use the `wasm` feature: polyproto = { version = "0", features = ["wasm"] } ``` +## HTTP API client through `reqwest` + +If the `reqwest` feature is activated, this crate offers a polyproto HTTP API client, using the +`reqwest` crate. + +### Alternatives to `reqwest` + +If you would like to implement an HTTP client using something other than `reqwest`, simply enable +the `types` and `serde` features. Using these features, you can implement your own HTTP client, with +the polyproto crate acting as a single source of truth for request and response types, as well as +request routes and methods through the exported `static` `Route`s. + [build-shield]: https://img.shields.io/github/actions/workflow/status/polyphony-chat/polyproto/build_and_test.yml?style=flat [build-url]: https://github.com/polyphony-chat/polyproto/blob/main/.github/workflows/build_and_test.yml [coverage-shield]: https://coveralls.io/repos/github/polyphony-chat/polyproto/badge.svg?branch=main diff --git a/examples/ed25519_basic.rs b/examples/ed25519_basic.rs index 2c6ff7f..21b7b6a 100644 --- a/examples/ed25519_basic.rs +++ b/examples/ed25519_basic.rs @@ -6,8 +6,6 @@ // This example is not complete and should not be copy-pasted into a production environment without // further scrutiny and consideration. -#![allow(unused)] - use std::str::FromStr; use der::asn1::BitString; @@ -17,10 +15,7 @@ use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; use rand::rngs::OsRng; use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; -use thiserror::Error; -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] fn main() { let mut csprng = rand::rngs::OsRng; // Generate a key pair @@ -60,10 +55,6 @@ fn main() { ) } -#[cfg(not(target_arch = "wasm32"))] -#[cfg(not(test))] -fn main() {} - // As mentioned in the README, we start by implementing the signature trait. // Here, we start by defining the signature type, which is a wrapper around the signature type from @@ -74,6 +65,12 @@ struct Ed25519Signature { algorithm: AlgorithmIdentifierOwned, } +impl std::fmt::Display for Ed25519Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.signature) + } +} + // We implement the Signature trait for our signature type. impl Signature for Ed25519Signature { // We define the signature type from the ed25519-dalek crate as the associated type. @@ -96,7 +93,7 @@ impl Signature for Ed25519Signature { } #[cfg(not(tarpaulin_include))] - fn from_bitstring(signature: &[u8]) -> Self { + fn from_bytes(signature: &[u8]) -> Self { let mut signature_vec = signature.to_vec(); signature_vec.resize(64, 0); let signature_array: [u8; 64] = { @@ -196,8 +193,6 @@ impl PublicKey for Ed25519PublicKey { fn try_from_public_key_info( public_key_info: PublicKeyInfo, ) -> Result { - use polyproto::errors::composite::ConversionError; - let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); key_vec.resize(32, 0); let signature_array: [u8; 32] = { @@ -210,3 +205,8 @@ impl PublicKey for Ed25519PublicKey { }) } } + +#[test] +fn test_example() { + main() +} diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index be2d7f2..bbcf929 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -2,25 +2,20 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -#![allow(unused)] - use std::str::FromStr; use std::time::Duration; -use der::asn1::{BitString, Ia5String, Uint, UtcTime}; +use der::asn1::{BitString, Uint, UtcTime}; use der::Encode; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; -use polyproto::certs::PublicKeyInfo; +use polyproto::certs::{PublicKeyInfo, Target}; use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; use rand::rngs::OsRng; use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; -use thiserror::Error; -use x509_cert::attr::Attributes; use x509_cert::name::RdnSequence; -use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; use x509_cert::Certificate; @@ -41,8 +36,6 @@ use x509_cert::Certificate; /// openssl x509 -in cert.der -text -noout -inform der /// ``` -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] fn main() { let mut csprng = rand::rngs::OsRng; let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); @@ -51,24 +44,24 @@ fn main() { println!(); let csr = polyproto::certs::idcsr::IdCsr::new( - &RdnSequence::from_str("CN=flori,DC=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), &priv_key, &Capabilities::default_actor(), + Some(Target::Actor), ) .unwrap(); let data = csr.clone().to_der().unwrap(); let file_name_with_extension = "cert.csr"; - #[cfg(not(target_arch = "wasm32"))] - std::fs::write(file_name_with_extension, &data).unwrap(); + std::fs::write(file_name_with_extension, data).unwrap(); let cert = IdCert::from_actor_csr( csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,DC=www,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", - ) - .unwrap(), + RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), Validity { not_before: Time::UtcTime( UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), @@ -82,13 +75,9 @@ fn main() { let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); let file_name_with_extension = "cert.der"; #[cfg(not(target_arch = "wasm32"))] - std::fs::write(file_name_with_extension, &data).unwrap(); + std::fs::write(file_name_with_extension, data).unwrap(); } -#[cfg(not(target_arch = "wasm32"))] -#[cfg(not(test))] -fn main() {} - // As mentioned in the README, we start by implementing the signature trait. // Here, we start by defining the signature type, which is a wrapper around the signature type from @@ -99,6 +88,12 @@ struct Ed25519Signature { algorithm: AlgorithmIdentifierOwned, } +impl std::fmt::Display for Ed25519Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.signature) + } +} + // We implement the Signature trait for our signature type. impl Signature for Ed25519Signature { // We define the signature type from the ed25519-dalek crate as the associated type. @@ -121,7 +116,7 @@ impl Signature for Ed25519Signature { } #[cfg(not(tarpaulin_include))] - fn from_bitstring(signature: &[u8]) -> Self { + fn from_bytes(signature: &[u8]) -> Self { let mut signature_vec = signature.to_vec(); signature_vec.resize(64, 0); let signature_array: [u8; 64] = { @@ -232,3 +227,8 @@ impl PublicKey for Ed25519PublicKey { }) } } + +#[test] +fn test_example() { + main() +} diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index 5a7c3f6..afac72d 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -2,27 +2,20 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -#![allow(unused)] - use std::str::FromStr; use std::time::Duration; -use der::asn1::{BitString, Ia5String, Uint, UtcTime}; -use der::{Decode, Encode}; +use der::asn1::{BitString, Uint, UtcTime}; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; -use polyproto::certs::PublicKeyInfo; +use polyproto::certs::{PublicKeyInfo, Target}; use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; use rand::rngs::OsRng; use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; -use thiserror::Error; -use x509_cert::attr::Attributes; use x509_cert::name::RdnSequence; -use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; -use x509_cert::Certificate; /// The following example uses the same setup as in ed25519_basic.rs, but in its main method, it /// creates a certificate signing request (CSR) and writes it to a file. The CSR is created from a @@ -34,35 +27,38 @@ use x509_cert::Certificate; /// openssl req -in cert.csr -verify -inform der /// ``` -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] fn main() { let mut csprng = rand::rngs::OsRng; - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - println!("Private Key is: {:?}", priv_key.key.to_bytes()); - println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); + let priv_key_actor = Ed25519PrivateKey::gen_keypair(&mut csprng); + let priv_key_home_server = Ed25519PrivateKey::gen_keypair(&mut csprng); + println!("Private Key is: {:?}", priv_key_actor.key.to_bytes()); + println!( + "Public Key is: {:?}", + priv_key_actor.public_key.key.to_bytes() + ); println!(); let csr = polyproto::certs::idcsr::IdCsr::new( - &RdnSequence::from_str("CN=flori,DC=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), - &priv_key, + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key_actor, &Capabilities::default_actor(), + Some(polyproto::certs::Target::Actor), ) .unwrap(); let data = csr.clone().to_der().unwrap(); let file_name_with_extension = "cert.csr"; #[cfg(not(target_arch = "wasm32"))] - std::fs::write(file_name_with_extension, &data).unwrap(); + std::fs::write(file_name_with_extension, data).unwrap(); let cert = IdCert::from_actor_csr( csr, - &priv_key, + &priv_key_home_server, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,DC=www,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", - ) - .unwrap(), + RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), Validity { not_before: Time::UtcTime( UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), @@ -74,14 +70,17 @@ fn main() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); - let cert_from_der = IdCert::from_der(data).unwrap(); - assert_eq!(cert_from_der, cert) + // ``::from_der()` performs a full check of the certificate, including signature verification. + let cert_from_der = + IdCert::from_der(&data, Target::Actor, 15, &priv_key_home_server.public_key).unwrap(); + assert_eq!(cert_from_der, cert); + // ...so technically, we don't need to verify the signature again. This is just for demonstration + // of how you would manually verify a certificate. + assert!(cert_from_der + .full_verify_actor(15, &priv_key_home_server.public_key) + .is_ok()) } -#[cfg(not(target_arch = "wasm32"))] -#[cfg(not(test))] -fn main() {} - // As mentioned in the README, we start by implementing the signature trait. // Here, we start by defining the signature type, which is a wrapper around the signature type from @@ -92,6 +91,12 @@ struct Ed25519Signature { algorithm: AlgorithmIdentifierOwned, } +impl std::fmt::Display for Ed25519Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.signature) + } +} + // We implement the Signature trait for our signature type. impl Signature for Ed25519Signature { // We define the signature type from the ed25519-dalek crate as the associated type. @@ -113,7 +118,7 @@ impl Signature for Ed25519Signature { } } - fn from_bitstring(signature: &[u8]) -> Self { + fn from_bytes(signature: &[u8]) -> Self { let mut signature_vec = signature.to_vec(); signature_vec.resize(64, 0); let signature_array: [u8; 64] = { @@ -222,3 +227,8 @@ impl PublicKey for Ed25519PublicKey { }) } } + +#[test] +fn test_example() { + main() +} diff --git a/examples/http_api.rs b/examples/http_api.rs new file mode 100644 index 0000000..569dd77 --- /dev/null +++ b/examples/http_api.rs @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use http::HeaderMap; +use httptest::matchers::request; +use httptest::responders::json_encoded; +use httptest::{Expectation, Server}; +use polyproto::api::core::current_unix_time; +use polyproto::api::HttpClient; +use polyproto::types::routes::core::v1::GET_CHALLENGE_STRING; +use serde_json::json; + +async fn setup_example() -> Server { + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path( + GET_CHALLENGE_STRING.method.as_str(), + GET_CHALLENGE_STRING.path, + )) + .respond_with(json_encoded(json!({ + "challenge": "abcd".repeat(8), + "expires": current_unix_time() + 100 + }))), + ); + server +} + +#[tokio::main] +async fn main() { + let server = setup_example().await; + let url = format!("http://{}", server.addr()); + + // The actual example starts here. + // Create a new HTTP client + let mut client = HttpClient::new(&url).unwrap(); + // Add an authorization header to the client + client.headers({ + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "my_secret_token".parse().unwrap()); + headers + }); + // You can now use the client to make requests to the polyproto home server! + // Routes are documented under , and each route has a + // corresponding method in the `HttpClient` struct. For example, if we wanted to get a challenge + // string from the server, we would call: + let challenge = client.get_challenge_string().await.unwrap(); + println!("Challenge string: {}", challenge.challenge); + println!("Challenge expires at UNIX timestamp: {}", challenge.expires); +} + +#[test] +fn test_example() {} diff --git a/firedbg/target-unit-test.json b/firedbg/target-unit-test.json new file mode 100644 index 0000000..636ddd0 --- /dev/null +++ b/firedbg/target-unit-test.json @@ -0,0 +1,6 @@ +{ + "binaries": [], + "examples": [], + "integration_tests": [], + "unit_tests": [] +} \ No newline at end of file diff --git a/firedbg/target.json b/firedbg/target.json new file mode 100644 index 0000000..0feda79 --- /dev/null +++ b/firedbg/target.json @@ -0,0 +1,36 @@ +{ + "binaries": [], + "examples": [ + { + "name": "ed25519_basic", + "src_path": "/Users/star/Codespace/polyphony/polyproto/examples/ed25519_basic.rs", + "required_features": [] + }, + { + "name": "ed25519_from_der", + "src_path": "/Users/star/Codespace/polyphony/polyproto/examples/ed25519_from_der.rs", + "required_features": [] + }, + { + "name": "ed25519_cert", + "src_path": "/Users/star/Codespace/polyphony/polyproto/examples/ed25519_cert.rs", + "required_features": [] + } + ], + "integration_tests": [ + { + "package_name": "polyproto", + "test": { + "name": "idcert", + "src_path": "/Users/star/Codespace/polyphony/polyproto/tests/idcert.rs", + "required_features": [] + }, + "test_cases": [ + "test_create_actor_cert", + "test_create_ca_cert", + "test_create_invalid_actor_csr" + ] + } + ], + "unit_tests": [] +} \ No newline at end of file diff --git a/firedbg/version.toml b/firedbg/version.toml new file mode 100644 index 0000000..db75b6b --- /dev/null +++ b/firedbg/version.toml @@ -0,0 +1 @@ +firedbg_cli = "1.77.1" diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs new file mode 100644 index 0000000..21a213f --- /dev/null +++ b/src/api/core/mod.rs @@ -0,0 +1,352 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::time::UNIX_EPOCH; + +use crate::types::x509_cert::SerialNumber; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::certs::idcert::IdCert; +use crate::certs::idcsr::IdCsr; +use crate::certs::{PublicKeyInfo, SessionId}; +use crate::errors::{ConversionError, RequestError}; +use crate::key::PublicKey; +use crate::signature::Signature; +use crate::types::routes::core::v1::*; +use crate::types::{ChallengeString, EncryptedPkm}; + +use super::{HttpClient, HttpResult}; + +/// Get the current UNIX timestamp according to the system clock. +pub fn current_unix_time() -> u64 { + std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} + +// Core Routes: No registration needed +impl HttpClient { + /// Request a [ChallengeString] from the server. + pub async fn get_challenge_string(&self) -> HttpResult { + let request_url = self.url.join(GET_CHALLENGE_STRING.path)?; + let request_response = self + .client + .request(GET_CHALLENGE_STRING.method.clone(), request_url) + .send() + .await; + HttpClient::handle_response(request_response).await + } + + /// Request the server to rotate its identity key and return the new [IdCert]. This route is + /// only available to server administrators. + /// + /// ## Safety guarantees + /// + /// The resulting [IdCert] is verified and has the same safety guarantees as specified under + /// [IdCert::full_verify_home_server()], as this method calls that method internally. + pub async fn rotate_server_identity_key>( + &self, + ) -> HttpResult> { + let request_url = self.url.join(ROTATE_SERVER_IDENTITY_KEY.path)?; + let request_response = self + .client + .request(ROTATE_SERVER_IDENTITY_KEY.method.clone(), request_url) + .send() + .await; + let pem = HttpClient::handle_response::(request_response).await?; + log::debug!("Received IdCert: \n{}", pem); + let id_cert = IdCert::::from_pem_unchecked(&pem)?; + match id_cert.full_verify_home_server( + std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ) { + Ok(_) => (), + Err(e) => return Err(RequestError::ConversionError(e.into())), + }; + Ok(id_cert) + } + + /// Request the server's public [IdCert]. Specify a unix timestamp to get the IdCert which was + /// valid at that time. If no timestamp is provided, the current IdCert is returned. + /// + /// ## Safety guarantees + /// + /// The resulting [IdCert] is verified and has the same safety guarantees as specified under + /// [IdCert::full_verify_home_server()], as this method calls that method internally. + pub async fn get_server_id_cert>( + &self, + unix_time: Option, + ) -> HttpResult> { + let request_url = self.url.join(GET_SERVER_PUBLIC_IDCERT.path)?; + let mut request = self + .client + .request(GET_SERVER_PUBLIC_IDCERT.method.clone(), request_url); + if let Some(time) = unix_time { + request = request.body(json!({ "timestamp": time }).to_string()); + } + let response = request.send().await; + let pem = HttpClient::handle_response::(response).await?; + let id_cert = IdCert::::from_pem_unchecked(&pem)?; + match id_cert.full_verify_home_server(unix_time.unwrap_or(current_unix_time())) { + Ok(_) => (), + Err(e) => return Err(RequestError::ConversionError(e.into())), + }; + Ok(id_cert) + } + + /// Request the server's [PublicKeyInfo]. Specify a unix timestamp to get the public key which + /// the home server used at that time. If no timestamp is provided, the current public key is + /// returned. + pub async fn get_server_public_key_info( + &self, + unix_time: Option, + ) -> HttpResult { + let request_url = self.url.join(GET_SERVER_PUBLIC_KEY.path)?; + let mut request = self + .client + .request(GET_SERVER_PUBLIC_KEY.method.clone(), request_url); + if let Some(time) = unix_time { + request = request.body(json!({ "timestamp": time }).to_string()); + } + let response = request.send().await; + let pem = HttpClient::handle_response::(response).await?; + Ok(PublicKeyInfo::from_pem(pem.as_str())?) + } + + /// Request the [IdCert]s of an actor. Specify the federation ID of the actor to get the IdCerts + /// of that actor. Returns a vector of IdCerts which were valid for the actor at the specified + /// time. If no timestamp is provided, the current IdCerts are returned. + /// + /// ## Safety guarantees + /// + /// The resulting [IdCert]s are not verified. The caller is responsible for verifying the correctness + /// of these `IdCert`s using [IdCert::full_verify_actor()] before using them. + pub async fn get_actor_id_certs>( + &self, + fid: &str, + unix_time: Option, + session_id: Option<&SessionId>, + ) -> HttpResult>> { + let request_url = self + .url + .join(&format!("{}{}", GET_ACTOR_IDCERTS.path, fid))?; + let mut request = self + .client + .request(GET_ACTOR_IDCERTS.method.clone(), request_url); + let body = match (unix_time, session_id) { + // PRETTYFYME + (Some(time), Some(session)) => { + Some(json!({ "timestamp": time, "session_id": session.to_string() })) + } + (Some(time), None) => Some(json!({"timestamp": time})), + (None, Some(session)) => Some(json!({"session_id": session.to_string()})), + (None, None) => None, + }; + if let Some(body) = body { + request = request.body(body.to_string()); + } + let response = request.send().await; + let pems = HttpClient::handle_response::>(response).await?; + let mut vec_idcert = Vec::new(); + for json in pems.into_iter() { + vec_idcert.push(IdCertExt::try_from(json)?); + } + Ok(vec_idcert) + } + + /// Inform a foreign server about a new [IdCert] for a session. + pub async fn update_session_id_cert>( + &self, + new_cert: IdCert, + ) -> HttpResult<()> { + let request_url = self.url.join(UPDATE_SESSION_IDCERT.path)?; + self.client + .request(UPDATE_SESSION_IDCERT.method.clone(), request_url) + .body(new_cert.to_pem(der::pem::LineEnding::LF)?) + .send() + .await?; + Ok(()) + } + + /// Tell a server to delete a session, revoking the session token. + pub async fn delete_session(&self, session_id: &SessionId) -> HttpResult<()> { + let request_url = self.url.join(DELETE_SESSION.path)?; + let body = json!({ "session_id": session_id.to_string() }); + self.client + .request(DELETE_SESSION.method.clone(), request_url) + .body(body.to_string()) + .send() + .await?; + Ok(()) + } +} + +// Core Routes: Registration needed +impl HttpClient { + /// Rotate your keys for a given session. The `session_id` in the supplied [IdCsr] must + /// correspond to the session token used in the authorization-Header. + /// + /// Returns the new [IdCert] and a token which can be used to authenticate future requests. + /// + /// ## Safety guarantees + /// + /// The resulting [IdCert] is not verified. The caller is responsible for verifying the correctness + /// of this `IdCert` using either [IdCert::full_verify_actor()] or [IdCert::full_verify_home_server()]. + pub async fn rotate_session_id_cert>( + &self, + csr: IdCsr, + ) -> HttpResult<(IdCert, String)> { + let request_url = self.url.join(ROTATE_SESSION_IDCERT.path)?; + let request_response = self + .client + .request(ROTATE_SESSION_IDCERT.method.clone(), request_url) + .body(csr.to_pem(der::pem::LineEnding::LF)?) + .send() + .await; + let response_value = HttpClient::handle_response::(request_response).await?; + let id_cert = IdCert::::from_pem_unchecked(&response_value.id_cert.to_string())?; + Ok((id_cert, response_value.token)) + } + + /// Upload encrypted private key material to the server for later retrieval. The upload size + /// must not exceed the server's maximum upload size for this route. This is usually not more + /// than 10kb and can be as low as 800 bytes, depending on the server configuration. + /// + /// The `data` parameter is a vector of [EncryptedPkm] which contains the serial number of the + /// ID-Cert and the encrypted private key material. Naturally, the server cannot check the + /// contents of the encrypted private key material. However, it is recommended to store the data + /// in a `SubjectPublicKeyInfo` structure, where the public key is the private key material. + pub async fn upload_encrypted_pkm(&self, data: Vec) -> HttpResult<()> { + let mut body = Vec::new(); + for pkm in data.iter() { + body.push(json!(pkm)); + } + let request_url = self.url.join(UPLOAD_ENCRYPTED_PKM.path)?; + self.client + .request(UPLOAD_ENCRYPTED_PKM.method.clone(), request_url) + .body(json!(body).to_string()) + .send() + .await?; + Ok(()) + } + + /// Retrieve encrypted private key material from the server. The serial_numbers, if provided, + /// must match the serial numbers of ID-Certs that the client has uploaded key material for. + /// If no serial_numbers are provided, the server will return all key material that the client + /// has uploaded. + pub async fn get_encrypted_pkm( + &self, + serials: Vec, + ) -> HttpResult> { + let request_url = self.url.join(GET_ENCRYPTED_PKM.path)?; + let mut body = Vec::new(); + for serial in serials.iter() { + body.push(json!(serial.try_as_u128()?)); + } + let request = self + .client + .request(GET_ENCRYPTED_PKM.method.clone(), request_url) + .body(json!(body).to_string()); + let response = + HttpClient::handle_response::>(request.send().await).await?; + let mut vec_pkm = Vec::new(); + for pkm in response.into_iter() { + vec_pkm.push(pkm); + } + Ok(vec_pkm) + } + + /// Delete encrypted private key material from the server. The serials must match the + /// serial numbers of ID-Certs that the client has uploaded key material for. + pub async fn delete_encrypted_pkm(&self, serials: Vec) -> HttpResult<()> { + let request_url = self.url.join(DELETE_ENCRYPTED_PKM.path)?; + let mut body = Vec::new(); + for serial in serials.iter() { + body.push(json!(serial.try_as_u128()?)); + } + self.client + .request(DELETE_ENCRYPTED_PKM.method.clone(), request_url) + .body(json!(body).to_string()) + .send() + .await?; + Ok(()) + } + + /// Retrieve the maximum upload size for encrypted private key material, in bytes. + pub async fn get_pkm_upload_size_limit(&self) -> HttpResult { + let request = self.client.request( + GET_ENCRYPTED_PKM_UPLOAD_SIZE_LIMIT.method.clone(), + self.url.join(GET_ENCRYPTED_PKM_UPLOAD_SIZE_LIMIT.path)?, + ); + let response = request.send().await; + HttpClient::handle_response::(response).await + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Represents an [IdCert] with an additional field `invalidated` which indicates whether the +/// certificate has been invalidated. This type is used in the API as a response to the +/// `GET /.p2/core/v1/idcert/actor/:fid` +/// route. Can be converted to and (try)from [IdCertExtJson]. +pub struct IdCertExt> { + /// The [IdCert] itself + pub id_cert: IdCert, + /// Whether the certificate has been marked as invalidated + pub invalidated: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +/// Stringly typed version of [IdCertExt], used for serialization and deserialization. +pub struct IdCertExtJson { + /// The [IdCert] as a PEM encoded string + pub id_cert: String, + /// Whether the certificate has been marked as invalidated + pub invalidated: bool, +} + +impl> From> for IdCertExtJson { + fn from(id_cert: IdCertExt) -> Self { + Self { + id_cert: id_cert.id_cert.to_pem(der::pem::LineEnding::LF).unwrap(), + invalidated: id_cert.invalidated, + } + } +} + +impl> TryFrom for IdCertExt { + type Error = ConversionError; + + fn try_from(id_cert: IdCertExtJson) -> Result { + Ok(Self { + id_cert: IdCert::from_pem_unchecked(id_cert.id_cert.as_str())?, + invalidated: id_cert.invalidated, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +/// Represents a pair of an [IdCert] and a token, used in the API as a response when an [IdCsr] has +/// been accepted by the server. +pub struct IdCertToken { + /// The [IdCert] as a PEM encoded string + pub id_cert: String, + /// The token as a string + pub token: String, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_challenge_string() { + let url = "https://example.com/"; + let client = HttpClient::new(url).unwrap(); + let _result = client.get_challenge_string(); + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..50eed8b --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,100 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use serde::Deserialize; +use serde_json::from_str; +use url::Url; + +use crate::errors::RequestError; + +/// The `core` module contains all API routes for implementing the core polyproto protocol in a client or server. +pub mod core; + +#[derive(Debug, Clone)] +/// A client for making HTTP requests to a polyproto home server. Stores headers such as the +/// authentication token, and the base URL of the server. Both the headers and the URL can be +/// modified after the client is created. However, the intended use case is to create one client +/// per actor, and use it for all requests made by that actor. +/// +/// # Example +/// +/// ```rs +/// let mut header_map = reqwest::header::HeaderMap::new(); +/// header_map.insert("Authorization", "nx8r902hjkxlo2n8n72x0"); +/// let client = HttpClient::new("https://example.com").unwrap(); +/// client.headers(header_map); +/// +/// let challenge: ChallengeString = client.get_challenge_string().await.unwrap(); +/// ``` +pub struct HttpClient { + /// The reqwest client used to make requests. + pub client: reqwest::Client, + headers: reqwest::header::HeaderMap, + pub(crate) url: Url, +} + +/// A type alias for the result of an HTTP request. +pub type HttpResult = Result; + +impl HttpClient { + /// Creates a new instance of the client with no further configuration. To access routes which + /// require authentication, you must set the authentication header using the `headers` method. + /// + /// # Arguments + /// + /// * `url` - The base URL of a polyproto home server. + pub fn new(url: &str) -> HttpResult { + let client = reqwest::Client::new(); + let headers = reqwest::header::HeaderMap::new(); + let url = Url::parse(url)?; + + Ok(Self { + client, + headers, + url, + }) + } + + /// Sets the headers for the client. + pub fn headers(&mut self, headers: reqwest::header::HeaderMap) { + self.headers = headers; + } + + /// Returns the URL + pub fn url(&self) -> String { + self.url.to_string() + } + + /// Sets the base URL of the client. + pub fn set_url(&mut self, url: &str) -> HttpResult<()> { + self.url = Url::parse(url)?; + Ok(()) + } + + /// Sends a request and returns the response. + pub async fn request>( + &self, + method: reqwest::Method, + url: &str, + body: Option, + ) -> HttpResult { + Url::parse(url)?; + let mut request = self.client.request(method, url); + request = request.headers(self.headers.clone()); + if let Some(body) = body { + request = request.body(body); + } + Ok(request.send().await?) + } + + /// Sends a request, handles the response, and returns the deserialized object. + pub(crate) async fn handle_response Deserialize<'a>>( + response: Result, + ) -> Result { + let response = response?; + let response_text = response.text().await?; + let object = from_str::(&response_text)?; + Ok(object) + } +} diff --git a/src/certs/capabilities/basic_constraints.rs b/src/certs/capabilities/basic_constraints.rs index 2edb532..c8fd644 100644 --- a/src/certs/capabilities/basic_constraints.rs +++ b/src/certs/capabilities/basic_constraints.rs @@ -6,12 +6,12 @@ use std::str::FromStr; use der::asn1::{OctetString, SequenceOf, SetOfVec}; use der::{Any, Decode, Encode, Tag, Tagged}; +use log::{trace, warn}; use spki::ObjectIdentifier; use x509_cert::attr::Attribute; use x509_cert::ext::Extension; -use crate::errors::base::{ConstraintError, InvalidInput}; -use crate::errors::composite::ConversionError; +use crate::errors::{ConstraintError, ConversionError, InvalidInput}; use super::OID_BASIC_CONSTRAINTS; @@ -52,12 +52,11 @@ impl TryFrom for BasicConstraints { fn try_from(value: Attribute) -> Result { // Basic input validation. Check OID of Attribute and length of the "values" SetOfVec provided. if value.oid.to_string() != super::OID_BASIC_CONSTRAINTS { - return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - format!( - "OID of value does not match any of OID_BASIC_CONSTRAINTS. Found OID {}", - value.oid - ), - ))); + return Err(InvalidInput::Malformed(format!( + "OID of value does not match any of OID_BASIC_CONSTRAINTS. Found OID {}", + value.oid + )) + .into()); } let values = value.values; if values.len() != 1usize { @@ -166,20 +165,24 @@ impl TryFrom for BasicConstraints { /// this property is critical, use the [Constrained] trait to verify the well-formedness of /// these resulting [BasicConstraints]. fn try_from(value: Extension) -> Result { + trace!("Converting Extension to BasicConstraints"); + trace!("Extension: {:#?}", value); #[allow(unreachable_patterns)] if value.critical && !matches!(value.extn_id.to_string().as_str(), OID_BASIC_CONSTRAINTS) { // Error if we encounter a "critical" X.509 extension which we do not know of + warn!("Unknown critical extension: {:#?}", value.extn_id); return Err(ConversionError::UnknownCriticalExtension { oid: value.extn_id }); } // If the Extension is a valid BasicConstraint, the octet string will contain DER ANY values // in a DER SET OF type let sequence: SequenceOf = SequenceOf::from_der(value.extn_value.as_bytes())?; if sequence.len() > 2 { + warn!( + "Encountered too many values in BasicConstraints. Found {} values", + sequence.len() + ); return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - format!( - "This x509_cert::Extension has {} values stored. Expected a maximum of 2 values", - sequence.len() - ), + format!("This x509_cert::Extension has {} values stored. Expected a maximum of 2 values", sequence.len()), ))); } let mut bool_encounters = 0u8; @@ -192,18 +195,22 @@ impl TryFrom for BasicConstraints { Tag::Boolean => { bool_encounters += 1; ca = any_to_bool(item.clone())?; - }, + } Tag::Integer => { int_encounters += 1; path_length = Some(any_to_u64(item.clone())?); - }, + } Tag::Null => { null_encounters += 1; path_length = None; - }, - _ => return Err(ConversionError::InvalidInput(InvalidInput::Malformed(format!("Encountered unexpected tag {:?}, when tag should have been either Boolean, Integer or Null", item.tag())))), + } + _ => { + warn!("Encountered unexpected tag: {:?}", item.tag()); + return Err(ConversionError::InvalidInput(InvalidInput::Malformed(format!("Encountered unexpected tag {:?}, when tag should have been either Boolean, Integer or Null", item.tag())))); + } } if bool_encounters > 1 || int_encounters > 1 || null_encounters > 1 { + warn!("Encountered too many values in BasicConstraints. BasicConstraints are likely malformed. BasicConstraints: {:#?}", value); return Err(ConversionError::InvalidInput(InvalidInput::Length { min_length: 0, max_length: 1, @@ -218,18 +225,23 @@ impl TryFrom for BasicConstraints { /// Tries to convert an [Any] value to a [bool]. fn any_to_bool(value: Any) -> Result { match value.tag() { - Tag::Boolean => { - match value.value() { - &[0x00] => Ok(false), - &[0xFF] | &[0x01] => Ok(true), - _ => { - Err( - ConstraintError::Malformed(Some("Encountered unexpected value for Boolean tag".to_string())), - ) - } + Tag::Boolean => match value.value() { + &[0x00] => Ok(false), + &[0xFF] | &[0x01] => Ok(true), + _ => { + warn!( + "Encountered unexpected value for Boolean tag: {:?}", + value.value() + ); + Err(ConstraintError::Malformed(Some( + "Encountered unexpected value for Boolean tag".to_string(), + ))) } }, - _ => Err(ConstraintError::Malformed(Some(format!("Found {:?} in value, which does not match expected [Tag::Boolean, Tag::Integer, Tag::Null]", value.tag().to_string())))), + _ => { + warn!("Encountered unexpected tag: {:?}", value.tag()); + Err(ConstraintError::Malformed(Some(format!("Found {:?} in value, which does not match expected [Tag::Boolean, Tag::Integer, Tag::Null]", value.tag().to_string())))) + } } } @@ -243,8 +255,11 @@ fn any_to_u64(value: Any) -> Result { let len = 8.min(value.value().len()); buf[..len].copy_from_slice(value.value()); Ok(u64::from_be_bytes(buf)) - }, - _ => Err(ConstraintError::Malformed(Some(format!("Found {:?} in value, which does not match expected [Tag::Boolean, Tag::Integer, Tag::Null]", value.tag().to_string())))), + } + _ => { + warn!("Encountered unexpected tag: {:?}", value.tag()); + Err(ConstraintError::Malformed(Some(format!("Found {:?} in value, which does not match expected [Tag::Boolean, Tag::Integer, Tag::Null]", value.tag().to_string())))) + } } } @@ -252,11 +267,14 @@ fn any_to_u64(value: Any) -> Result { #[allow(clippy::unwrap_used)] #[cfg(test)] mod test { + use crate::testing_utils::init_logger; + use super::*; #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] fn basic_constraints_to_extension() { + init_logger(); let basic_constraints = BasicConstraints { ca: true, path_length: Some(0u64), @@ -267,6 +285,7 @@ mod test { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] fn extension_to_basic_constraints() { + init_logger(); let basic_constraints = BasicConstraints { ca: true, path_length: Some(u64::MAX), diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 1b6fec2..be7e6f7 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -10,8 +10,7 @@ use spki::ObjectIdentifier; use x509_cert::attr::Attribute; use x509_cert::ext::Extension; -use crate::errors::base::InvalidInput; -use crate::errors::composite::ConversionError; +use crate::errors::{ConversionError, InvalidInput}; use super::*; @@ -91,13 +90,15 @@ impl KeyUsages { /// ``` pub fn from_bitstring(bitstring: BitString) -> Result { let mut byte_array = bitstring.raw_bytes().to_vec(); - if byte_array.is_empty() || byte_array.len() < 2 { - return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - "Passed BitString seems to be invalid".to_string(), - ))); - } - byte_array.remove(0); + log::trace!("[from_bitstring] BitString raw bytes: {:?}", byte_array); let mut key_usages = Vec::new(); + if byte_array == [0] || byte_array.is_empty() { + // TODO: PLEASE write a test for this. Is an empty byte array valid? Is a byte array with a single 0 valid, and does it mean that no KeyUsage is set? -bitfl0wer + return Ok(KeyUsages { key_usages }); + } + if byte_array[0] == 0 && byte_array.len() == 2 { + byte_array.remove(0); + } if byte_array.len() == 2 { // If the length of the byte array is 2, this means that DecipherOnly is set. key_usages.push(KeyUsage::DecipherOnly); @@ -123,17 +124,18 @@ impl KeyUsages { // This should never happen, as we are only dividing by 2 until we reach 1. _ => panic!("This should never happen. Please report this error to https://github.com/polyphony-chat/polyproto"), }) - } - if current_try == 1 { + } else if current_try == 1 { break; + } else { + current_try /= 2; } - current_try /= 2; } if byte_array[0] != 0 { return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - "Could not properly convert this BitString to KeyUsages. The BitString is malformed".to_string(), + "Could not properly convert this BitString to KeyUsages. The BitString contains a value not representable by KeyUsages".to_string(), ))); } + log::debug!("[from_bitstring] Converted KeyUsages: {:?}", key_usages); Ok(KeyUsages { key_usages }) } @@ -187,6 +189,8 @@ impl KeyUsages { // bits. unused_bits = 7; } + log::debug!("[to_bitstring] Unused bits: {}", unused_bits); + log::debug!("[to_bitstring] Encoded values: {:?}", encoded_numbers_vec); BitString::new(unused_bits, encoded_numbers_vec) .expect("Error when converting KeyUsages to BitString. Please report this error to https://github.com/polyphony-chat/polyproto") } @@ -202,9 +206,9 @@ impl TryFrom for KeyUsages { type Error = ConversionError; fn try_from(value: Attribute) -> Result { - if value.tag() != Tag::BitString { + if value.tag() != Tag::Sequence { return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - format!("Expected BitString, found {}", value.tag(),), + format!("Expected Sequence, found {}", value.tag(),), ))); } match value.values.len() { @@ -219,7 +223,8 @@ impl TryFrom for KeyUsages { } }; let inner_value = value.values.get(0).expect("Illegal state. Please report this error to https://github.com/polyphony-chat/polyproto"); - KeyUsages::from_bitstring(BitString::from_der(inner_value.value())?) + log::debug!("Inner value: {:?}", inner_value); + KeyUsages::from_bitstring(BitString::from_der(&inner_value.to_der()?)?) } } @@ -236,7 +241,7 @@ impl TryFrom for KeyUsages { ))); } let any = Any::from_der(value.extn_value.as_bytes())?; - KeyUsages::from_bitstring(BitString::from_bytes(any.value())?) + KeyUsages::from_bitstring(BitString::from_der(&any.to_der()?)?) } } @@ -246,7 +251,8 @@ impl TryFrom for Attribute { fn try_from(value: KeyUsages) -> Result { let mut sov = SetOfVec::new(); let bitstring = value.to_bitstring(); - sov.insert(Any::from_der(&bitstring.to_der()?)?)?; + let any = Any::from_der(&bitstring.to_der()?)?; + sov.insert(any)?; Ok(Attribute { oid: ObjectIdentifier::from_str(OID_KEY_USAGE)?, values: sov, diff --git a/src/certs/capabilities/mod.rs b/src/certs/capabilities/mod.rs index a0a583f..04d8c9f 100644 --- a/src/certs/capabilities/mod.rs +++ b/src/certs/capabilities/mod.rs @@ -15,9 +15,10 @@ use der::asn1::SetOfVec; use x509_cert::attr::{Attribute, Attributes}; use x509_cert::ext::{Extension, Extensions}; -use crate::errors::base::InvalidInput; -use crate::errors::composite::ConversionError; -use crate::Constrained; +use crate::{ + errors::{ConversionError, InvalidInput}, + Constrained, +}; /// Object Identifier for the KeyUsage::DigitalSignature variant. pub const OID_KEY_USAGE_DIGITAL_SIGNATURE: &str = "1.3.6.1.5.5.7.3.3"; @@ -140,7 +141,7 @@ impl TryFrom for Attributes { /// /// Fails, if `Capabilities::verify()` using the `Constrained` trait fails. fn try_from(value: Capabilities) -> Result { - value.validate()?; + value.validate(None)?; let mut sov = SetOfVec::new(); let insertion = sov.insert(Attribute::try_from(value.key_usage)?); if insertion.is_err() { diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 2c0eda0..b3dec43 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -3,20 +3,20 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use der::asn1::Uint; -use der::{Decode, Encode}; +use der::pem::LineEnding; +use der::{Decode, DecodePem, Encode, EncodePem}; use x509_cert::name::Name; use x509_cert::time::Validity; use x509_cert::Certificate; -use crate::errors::base::InvalidInput; -use crate::errors::composite::ConversionError; +use crate::errors::{ConstraintError, ConversionError, InvalidCert, ERR_CERTIFICATE_TO_DER_ERROR}; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; use crate::Constrained; -use super::equal_domain_components; use super::idcerttbs::IdCertTbs; use super::idcsr::IdCsr; +use super::Target; /// A signed polyproto ID-Cert, consisting of the actual certificate, the CA-generated signature and /// metadata about that signature. @@ -29,6 +29,15 @@ use super::idcsr::IdCsr; /// - **S**: The [Signature] and - by extension - [SignatureAlgorithm] this certificate was /// signed with. /// - **P**: A [PublicKey] type P which can be used to verify [Signature]s of type S. +/// +/// ## Verifying an ID-Cert +/// +/// To verify an ID-Cert, you can use the [full_verify_actor()] or [full_verify_home_server()] methods. +/// These methods will check if the certificate is valid at a given time, if the signature is correct, +/// and if the certificate is well-formed and up to polyproto specification. +/// +/// If you only need to check +/// if the certificate is valid at a given time, you can use the [valid_at()] method. #[derive(Debug, PartialEq, Eq, Clone)] pub struct IdCert> { /// Inner TBS (To be signed) certificate @@ -40,10 +49,24 @@ pub struct IdCert> { impl> IdCert { /// Create a new [IdCert] by passing an [IdCsr] and other supplementary information. Returns /// an error, if the provided IdCsr or issuer [Name] do not pass [Constrained] verification, - /// i.e. if they are not up to polyproto specification. Also fails if the provided IdCsr has - /// the [BasicConstraints] "ca" flag set to `false`. + /// i.e. if they are not up to polyproto specification. /// /// See [IdCert::from_actor_csr()] when trying to create a new actor certificate. + /// + /// ## Safety guarantees + /// + /// The resulting `IdCert` is guaranteed to be well-formed and up to polyproto specification, + /// for the usage context of a home server certificate. Assuming that cryptography has been + /// implemented correctly, the certificate is also guaranteed to have a valid signature. For a + /// more detailed list of guarantees, see [IdCert::full_home_server()]. + /// + /// ## Parameters + /// + /// - `id_csr`: The [IdCsr] to create the new certificate from. + /// - `signing_key`: The home server's private key, used to sign the new certificate. + /// - `serial_number`: The serial number that should be assigned to the new certificate. + /// - `issuer`: The [Name] of the issuer of the resulting certificate. + /// - `validity`: The [Validity] period of the resulting certificate. pub fn from_ca_csr( id_csr: IdCsr, signing_key: &impl PrivateKey, @@ -51,30 +74,46 @@ impl> IdCert { issuer: Name, validity: Validity, ) -> Result { - // IdCsr gets validated in IdCertTbs::from_..._csr let signature_algorithm = signing_key.algorithm_identifier(); - if !equal_domain_components(&id_csr.inner_csr.subject, &issuer) { - return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - "Domain components of the issuer and the subject do not match".to_string(), - ))); - } - let id_cert_tbs = - IdCertTbs::from_ca_csr(id_csr, serial_number, signature_algorithm, issuer, validity)?; + let id_cert_tbs = IdCertTbs:: { + serial_number, + signature_algorithm, + issuer, + validity, + subject: id_csr.inner_csr.subject, + subject_public_key: id_csr.inner_csr.subject_public_key, + capabilities: id_csr.inner_csr.capabilities, + s: std::marker::PhantomData, + }; let signature = signing_key.sign(&id_cert_tbs.clone().to_der()?); let cert = IdCert { id_cert_tbs, signature, }; - cert.validate()?; + cert.validate(Some(Target::HomeServer))?; Ok(cert) } /// Create a new [IdCert] by passing an [IdCsr] and other supplementary information. Returns /// an error, if the provided IdCsr or issuer [Name] do not pass [Constrained] verification, - /// i.e. if they are not up to polyproto specification. Also fails if the provided IdCsr has - /// the [BasicConstraints] "ca" flag set to `false`. + /// i.e. if they are not up to polyproto specification. /// /// See [IdCert::from_ca_csr()] when trying to create a new ca certificate. + /// + /// ## Safety guarantees + /// + /// The resulting `IdCert` is guaranteed to be well-formed and up to polyproto specification, + /// for the usage context of an actor certificate. Assuming that cryptography has been + /// implemented correctly, the certificate is also guaranteed to have a valid signature. For a + /// more detailed list of guarantees, see [IdCert::full_verify_actor()]. + /// + /// ## Parameters + /// + /// - `id_csr`: The [IdCsr] to create the new certificate from. + /// - `signing_key`: The home server's private key, used to sign the new certificate. + /// - `serial_number`: The serial number that should be assigned to the new certificate. + /// - `issuer`: The [Name] of the issuer of the resulting certificate. + /// - `validity`: The [Validity] period of the resulting certificate. pub fn from_actor_csr( id_csr: IdCsr, signing_key: &impl PrivateKey, @@ -82,36 +121,71 @@ impl> IdCert { issuer: Name, validity: Validity, ) -> Result { - // IdCsr gets validated in IdCertTbs::from_..._csr + log::trace!("[IdCert::from_actor_csr()] creating actor certificate"); let signature_algorithm = signing_key.algorithm_identifier(); - issuer.validate()?; - if !equal_domain_components(&id_csr.inner_csr.subject, &issuer) { - return Err(ConversionError::InvalidInput( - crate::errors::base::InvalidInput::Malformed( - "Domain components of the issuer and the subject do not match".to_string(), - ), - )); - } - let id_cert_tbs = IdCertTbs::from_actor_csr( - id_csr, + log::trace!("[IdCert::from_actor_csr()] creating IdCertTbs"); + log::trace!("[IdCert::from_actor_csr()] Issuer: {}", issuer.to_string()); + log::trace!( + "[IdCert::from_actor_csr()] Subject: {}", + id_csr.inner_csr.subject.to_string() + ); + let id_cert_tbs = IdCertTbs:: { serial_number, signature_algorithm, issuer, validity, - )?; + subject: id_csr.inner_csr.subject, + subject_public_key: id_csr.inner_csr.subject_public_key, + capabilities: id_csr.inner_csr.capabilities, + s: std::marker::PhantomData, + }; + log::trace!("[IdCert::from_actor_csr()] creating Signature"); let signature = signing_key.sign(&id_cert_tbs.clone().to_der()?); let cert = IdCert { id_cert_tbs, signature, }; - cert.validate()?; + log::trace!( + "[IdCert::from_actor_csr()] validating certificate with target {:?}", + Some(Target::Actor) + ); + cert.validate(Some(Target::Actor))?; Ok(cert) } - /// Create an IdCsr from a byte slice containing a DER encoded X.509 Certificate. - pub fn from_der(value: Vec) -> Result { - let cert = IdCert::try_from(Certificate::from_der(&value)?)?; - cert.validate()?; + /// Create an [IdCert] from a byte slice containing a DER encoded X.509 Certificate. + /// The resulting `IdCert` has the same validity guarantees as when using [IdCert::full_verify_actor()] + /// or [IdCert::full_verify_home_server()]. + pub fn from_der( + value: &[u8], + target: Target, + time: u64, + home_server_public_key: &P, + ) -> Result { + let cert = match IdCert::from_der_unchecked(value) { + Ok(cert) => cert, + Err(e) => { + return Err(InvalidCert::InvalidProperties(ConstraintError::Malformed( + Some(e.to_string()), + ))) + } + }; + match target { + Target::Actor => { + cert.full_verify_actor(time, home_server_public_key)?; + } + Target::HomeServer => { + cert.full_verify_home_server(time)?; + } + } + Ok(cert) + } + + /// Create an unchecked [IdCert] from a byte slice containing a DER encoded X.509 Certificate. + /// The caller is responsible for verifying the correctness of this `IdCert` using + /// the [Constrained] trait before using it. + pub fn from_der_unchecked(value: &[u8]) -> Result { + let cert = IdCert::try_from(Certificate::from_der(value)?)?; Ok(cert) } @@ -119,6 +193,126 @@ impl> IdCert { pub fn to_der(self) -> Result, ConversionError> { Ok(Certificate::try_from(self)?.to_der()?) } + + /// Create an [IdCert] from a byte slice containing a PEM encoded X.509 Certificate. + /// The resulting `IdCert` has the same validity guarantees as when using [IdCert::full_verify_actor()] + /// or [IdCert::full_verify_home_server()]. + pub fn from_pem( + pem: &str, + target: Target, + time: u64, + home_server_public_key: &P, + ) -> Result { + let cert = match IdCert::from_pem_unchecked(pem) { + Ok(cert) => cert, + Err(e) => { + return Err(InvalidCert::InvalidProperties(ConstraintError::Malformed( + Some(e.to_string()), + ))) + } + }; + match target { + Target::Actor => { + cert.full_verify_actor(time, home_server_public_key)?; + } + Target::HomeServer => { + cert.full_verify_home_server(time)?; + } + } + Ok(cert) + } + + /// Create an unchecked [IdCert] from a byte slice containing a PEM encoded X.509 Certificate. + /// The caller is responsible for verifying the correctness of this `IdCert` using + /// either [IdCert::full_verify_actor()] or [IdCert::full_verify_home_server()] before using it. + pub fn from_pem_unchecked(pem: &str) -> Result { + let cert = IdCert::try_from(Certificate::from_pem(pem)?)?; + Ok(cert) + } + + /// Encode this type as PEM, returning a string. + pub fn to_pem(self, line_ending: LineEnding) -> Result { + Ok(Certificate::try_from(self)?.to_pem(line_ending)?) + } + + /// Returns a byte vector containing the DER encoded IdCertTbs. This data is encoded + /// in the signature field of the certificate, and can be used to verify the signature. + /// + /// This is a shorthand for `self.id_cert_tbs.clone().to_der()`, since intuitively, one might + /// try to verify the signature of the certificate by using `self.to_der()`, which will result + /// in an error. + pub fn signature_data(&self) -> Result, ConversionError> { + self.id_cert_tbs.clone().to_der() + } + + /// Checks, if the certificate is valid at a given time. Does not check if the certificate is + /// well-formed, up to polyproto specification or if the signature is correct. If you need to + /// verify these properties, use either [IdCert::full_verify_actor()] or [IdCert::full_verify_home_server()] + /// instead. + pub fn valid_at(&self, time: u64) -> bool { + self.id_cert_tbs.valid_at(time) + } + + /// Performs verification of the certificate, checking for the following properties: + /// + /// - The certificate is valid at the given `time` + /// - The signature of the certificate is correct + /// - The certificate is well-formed and up to polyproto specification + /// - All parts that make up the certificate are well-formed and up to polyproto specification + pub fn full_verify_actor( + &self, + time: u64, + home_server_public_key: &P, + ) -> Result<(), InvalidCert> { + if !self.valid_at(time) { + return Err(InvalidCert::InvalidValidity); + } + log::trace!("[IdCert::full_verify_actor(&self)] verifying signature (actor certificate)"); + let der = match self.id_cert_tbs.clone().to_der() { + Ok(der) => der, + Err(_) => { + log::warn!( + "[IdCert::full_verify_actor(&self)] {}", + ERR_CERTIFICATE_TO_DER_ERROR + ); + return Err(InvalidCert::InvalidProperties(ConstraintError::Malformed( + Some(ERR_CERTIFICATE_TO_DER_ERROR.to_string()), + ))); + } + }; + Ok(home_server_public_key.verify_signature(&self.signature, &der)?) + } + + /// Performs verification of the certificate, checking for the following properties: + /// + /// - The certificate is valid at the given `time` + /// - The signature of the certificate is correct + /// - The certificate is well-formed and up to polyproto specification + /// - All parts that make up the certificate are well-formed and up to polyproto specification + pub fn full_verify_home_server(&self, time: u64) -> Result<(), InvalidCert> { + if !self.valid_at(time) { + return Err(InvalidCert::InvalidValidity); + } + let der = match self.id_cert_tbs.clone().to_der() { + Ok(data) => data, + Err(_) => { + log::warn!( + "[IdCert::full_verify_home_server(&self)] {}", + ERR_CERTIFICATE_TO_DER_ERROR + ); + return Err(InvalidCert::InvalidProperties(ConstraintError::Malformed( + Some(ERR_CERTIFICATE_TO_DER_ERROR.to_string()), + ))); + } + }; + log::trace!( + "[IdCert::full_verify_home_server(&self)] verifying signature (self-signed IdCert)" + ); + Ok(self + .id_cert_tbs + .subject_public_key + .verify_signature(&self.signature, &der)?) + } } impl> TryFrom> for Certificate { @@ -134,13 +328,17 @@ impl> TryFrom> for Certificate { impl> TryFrom for IdCert { type Error = ConversionError; - + /// Tries to convert a [Certificate] into an [IdCert]. The Ok() variant of this method + /// contains the `IdCert` if the conversion was successful. If this conversion is called + /// manually, the caller is responsible for verifying the correctness of this `IdCert` using + /// the [Constrained] trait. fn try_from(value: Certificate) -> Result { let id_cert_tbs = value.tbs_certificate.try_into()?; - let signature = S::from_bitstring(value.signature.raw_bytes()); - Ok(IdCert { + let signature = S::from_bytes(value.signature.raw_bytes()); + let cert = IdCert { id_cert_tbs, signature, - }) + }; + Ok(cert) } } diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 5814761..ef29eab 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -12,15 +12,14 @@ use x509_cert::serial_number::SerialNumber; use x509_cert::time::Validity; use x509_cert::TbsCertificate; -use crate::errors::base::InvalidInput; -use crate::errors::composite::ConversionError; +use crate::errors::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; use crate::Constrained; use super::capabilities::Capabilities; use super::idcsr::IdCsr; -use super::PublicKeyInfo; +use super::{PublicKeyInfo, Target}; /// An unsigned polyproto ID-Cert. /// @@ -59,7 +58,7 @@ pub struct IdCertTbs> { /// Capabilities assigned to the subject of the certificate. pub capabilities: Capabilities, /// PhantomData - s: std::marker::PhantomData, + pub(crate) s: std::marker::PhantomData, } impl> IdCertTbs { @@ -69,26 +68,18 @@ impl> IdCertTbs { /// the [BasicConstraints] "ca" flag set to `true`. /// /// See [IdCertTbs::from_ca_csr()] when trying to create a new CA certificate for home servers. - pub(crate) fn from_actor_csr( + /// + /// The resulting `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, + /// for the usage context of an actor certificate. + pub fn from_actor_csr( id_csr: IdCsr, serial_number: Uint, signature_algorithm: AlgorithmIdentifierOwned, issuer: Name, validity: Validity, ) -> Result { - if id_csr.inner_csr.capabilities.basic_constraints.ca { - return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - "Actor ID-Cert cannot have \"CA\" BasicConstraint set to true".to_string(), - ))); - } - id_csr.validate()?; - issuer.validate()?; - // Verify if signature of IdCsr matches contents - id_csr.inner_csr.subject_public_key.verify_signature( - &id_csr.signature, - id_csr.inner_csr.clone().to_der()?.as_slice(), - )?; - Ok(IdCertTbs { + id_csr.validate(Some(Target::Actor))?; + let cert_tbs = IdCertTbs { serial_number, signature_algorithm, issuer, @@ -97,7 +88,9 @@ impl> IdCertTbs { subject_public_key: id_csr.inner_csr.subject_public_key, capabilities: id_csr.inner_csr.capabilities, s: std::marker::PhantomData, - }) + }; + cert_tbs.validate(Some(Target::Actor))?; + Ok(cert_tbs) } /// Create a new [IdCertTbs] by passing an [IdCsr] and other supplementary information. Returns @@ -106,25 +99,18 @@ impl> IdCertTbs { /// the [BasicConstraints] "ca" flag set to `false`. /// /// See [IdCertTbs::from_actor_csr()] when trying to create a new actor certificate. - pub(crate) fn from_ca_csr( + /// + /// The resulting `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, + /// for the usage context of a home server certificate. + pub fn from_ca_csr( id_csr: IdCsr, serial_number: Uint, signature_algorithm: AlgorithmIdentifierOwned, issuer: Name, validity: Validity, ) -> Result { - if !id_csr.inner_csr.capabilities.basic_constraints.ca { - return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - "CA ID-Cert must have \"CA\" BasicConstraint set to true".to_string(), - ))); - } - id_csr.validate()?; - // Verify if signature of IdCsr matches contents - id_csr.inner_csr.subject_public_key.verify_signature( - &id_csr.signature, - id_csr.inner_csr.clone().to_der()?.as_slice(), - )?; - Ok(IdCertTbs { + id_csr.validate(Some(Target::HomeServer))?; + let cert_tbs = IdCertTbs { serial_number, signature_algorithm, issuer, @@ -133,7 +119,9 @@ impl> IdCertTbs { subject_public_key: id_csr.inner_csr.subject_public_key, capabilities: id_csr.inner_csr.capabilities, s: std::marker::PhantomData, - }) + }; + cert_tbs.validate(Some(Target::HomeServer))?; + Ok(cert_tbs) } /// Encode this type as DER, returning a byte vector. @@ -141,9 +129,28 @@ impl> IdCertTbs { Ok(TbsCertificate::try_from(self)?.to_der()?) } - /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. - pub fn from_der(bytes: &[u8]) -> Result { - IdCertTbs::try_from(TbsCertificate::from_der(bytes)?) + /// Create an [IdCertTbs] from a byte slice containing a DER encoded PKCS #10 CSR. The resulting + /// `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, + /// if the correct [Target] for the certificates' intended usage context is provided. + pub fn from_der(bytes: &[u8], target: Option) -> Result { + let cert = IdCertTbs::from_der_unchecked(bytes)?; + cert.validate(target)?; + Ok(cert) + } + + /// Create an unchecked [IdCertTbs] from a byte slice containing a DER encoded PKCS #10 CSR. The caller is + /// responsible for verifying the correctness of this `IdCertTbs` using + /// the [Constrained] trait before using it. + pub fn from_der_unchecked(bytes: &[u8]) -> Result { + let cert = IdCertTbs::try_from(TbsCertificate::from_der(bytes)?)?; + Ok(cert) + } + + /// Checks if the IdCertTbs was valid at a given UNIX time. Does not validate the certificate + /// against the polyproto specification. + pub(crate) fn valid_at(&self, time: u64) -> bool { + time >= self.validity.not_before.to_unix_duration().as_secs() + && time <= self.validity.not_after.to_unix_duration().as_secs() } } @@ -152,8 +159,11 @@ impl> TryFrom> { type Error = ConversionError; + /// Tries to convert a [TbsCertificateInner] into an [IdCertTbs]. The Ok() variant of this Result + /// is an unverified `IdCertTbs`. If this conversion is called manually, the caller is responsible + /// for verifying the `IdCertTbs` using the [Constrained] trait. fn try_from(value: TbsCertificateInner

) -> Result { - value.subject.validate()?; + value.subject.validate(None)?; let capabilities = match value.extensions { diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index dec3670..1c7fae6 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -4,19 +4,20 @@ use std::marker::PhantomData; -use der::{Decode, Encode}; +use der::pem::LineEnding; +use der::{Decode, DecodePem, Encode, EncodePem}; use spki::AlgorithmIdentifierOwned; use x509_cert::attr::Attributes; use x509_cert::name::Name; use x509_cert::request::{CertReq, CertReqInfo}; -use crate::errors::composite::ConversionError; +use crate::errors::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; -use crate::{Constrained, ConstraintError}; +use crate::Constrained; use super::capabilities::Capabilities; -use super::{PkcsVersion, PublicKeyInfo}; +use super::{PkcsVersion, PublicKeyInfo, Target}; #[derive(Debug, Clone, PartialEq, Eq)] /// A polyproto Certificate Signing Request, compatible with [IETF RFC 2986 "PKCS #10"](https://datatracker.ietf.org/doc/html/rfc2986). @@ -40,7 +41,6 @@ pub struct IdCsr> { /// [Signature] value for the `inner_csr` pub signature: S, } - impl> IdCsr { /// Performs basic input validation and creates a new polyproto ID-Cert CSR, according to /// PKCS#10. The CSR is being signed using the subjects' supplied signing key ([PrivateKey]) @@ -53,64 +53,95 @@ impl> IdCsr { /// on how many subdomain levels there are. /// - Domain Component: Actor home server domain. /// - Domain Component: Actor home server TLD, if applicable. - /// - Organizational Unit: Optional. May be repeated. + /// - Session ID: [SessionId], an Ia5String, max 32 characters. You can use the [SessionId] struct + /// and its [SessionId::new_validated()] and [SessionId::to_rdn_sequence()] methods + /// to help you create a valid SessionId. /// - **signing_key**: Subject signing key. Will NOT be included in the certificate. Is used to /// sign the CSR. - /// - **subject_unique_id**: [Uint], subject (actor) session ID. MUST NOT exceed 32 characters - /// in length. + /// - **capabilities**: The capabilities requested by the subject. + /// - **target**: The [Target] for which the CSR is intended. This is used to validate the CSR + /// against the polyproto specification. + /// + /// The resulting `IdCsr` is guaranteed to be well-formed and up to polyproto specification, + /// if the correct [Target] for the CSRs intended usage context is provided. pub fn new( subject: &Name, signing_key: &impl PrivateKey, capabilities: &Capabilities, + target: Option, ) -> Result, ConversionError> { - subject.validate()?; - let inner_csr = IdCsrInner::::new(subject, signing_key.pubkey(), capabilities)?; + let inner_csr = IdCsrInner:: { + version: PkcsVersion::V1, + subject: subject.clone(), + subject_public_key: signing_key.pubkey().clone(), + capabilities: capabilities.clone(), + phantom_data: PhantomData, + }; let signature = signing_key.sign(&inner_csr.clone().to_der()?); let signature_algorithm = S::algorithm_identifier(); - - Ok(IdCsr { + let id_csr = IdCsr { inner_csr, signature_algorithm, signature, - }) - } - - /// Validates the well-formedness of the [IdCsr] and its contents. Fails, if the [Name] or - /// [Capabilities] do not meet polyproto validation criteria for actor CSRs, or if - /// the signature fails to be verified. - - pub fn valid_actor_csr(&self) -> Result<(), ConversionError> { - self.validate()?; - if self.inner_csr.capabilities.basic_constraints.ca { - return Err(ConversionError::ConstraintError( - ConstraintError::Malformed(Some("Actor CSR must not be a CA".to_string())), - )); - } - Ok(()) + }; + log::trace!("[IdCsr::new()] Validating self with Target: {:?}", target); + id_csr.validate(target)?; + Ok(id_csr) } - /// Validates the well-formedness of the [IdCsr] and its contents. Fails, if the [Name] or - /// [Capabilities] do not meet polyproto validation criteria for home server CSRs, or if - /// the signature fails to be verified. - pub fn valid_home_server_csr(&self) -> Result<(), ConversionError> { - self.validate()?; - if !self.inner_csr.capabilities.basic_constraints.ca { - return Err(ConversionError::ConstraintError( - ConstraintError::Malformed(Some("Actor CSR must be a CA".to_string())), - )); - } - Ok(()) + /// Create an [IdCsr] from a byte slice containing a DER encoded PKCS #10 CSR. + /// The resulting `IdCsr` is guaranteed to be well-formed and up to polyproto specification, + /// if the correct [Target] for the CSRs intended usage context is provided. + pub fn from_der(bytes: &[u8], target: Option) -> Result { + let csr = IdCsr::from_der_unchecked(bytes)?; + csr.validate(target)?; + Ok(csr) } - /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. - pub fn from_der(bytes: &[u8]) -> Result { - IdCsr::try_from(CertReq::from_der(bytes)?) + /// Create an unchecked [IdCsr] from a byte slice containing a DER encoded PKCS #10 CSR. + /// The caller is responsible for verifying the correctness of this `IdCsr` using + /// the [Constrained] trait before using it. + pub fn from_der_unchecked(bytes: &[u8]) -> Result { + let csr = IdCsr::try_from(CertReq::from_der(bytes)?)?; + Ok(csr) } /// Encode this type as DER, returning a byte vector. pub fn to_der(self) -> Result, ConversionError> { Ok(CertReq::try_from(self)?.to_der()?) } + + /// Create an [IdCsr] from a string containing a PEM encoded PKCS #10 CSR. + /// The resulting `IdCsr` is guaranteed to be well-formed and up to polyproto specification, + /// if the correct [Target] for the CSRs intended usage context is provided. + pub fn from_pem(pem: &str, target: Option) -> Result { + let csr = IdCsr::from_pem_unchecked(pem)?; + csr.validate(target)?; + Ok(csr) + } + + /// Create an unchecked [IdCsr] from a string containing a PEM encoded PKCS #10 CSR. + /// The caller is responsible for verifying the correctness of this `IdCsr` using + /// the [Constrained] trait before using it. + pub fn from_pem_unchecked(pem: &str) -> Result { + let csr = IdCsr::try_from(CertReq::from_pem(pem)?)?; + Ok(csr) + } + + /// Encode this type as PEM, returning a string. + pub fn to_pem(self, line_ending: LineEnding) -> Result { + Ok(CertReq::try_from(self)?.to_pem(line_ending)?) + } + + /// Returns a byte vector containing the DER encoded [IdCsrInner]. This data is encoded + /// in the signature field of the IdCSR, and can be used to verify the signature of the CSR. + /// + /// This is a shorthand for `self.inner_csr.clone().to_der()`, since intuitively, one might + /// try to verify the signature of the CSR by using `self.to_der()`, which will result + /// in an error. + pub fn signature_data(&self) -> Result, ConversionError> { + self.inner_csr.clone().to_der() + } } /// In the context of PKCS #10, this is a `CertificationRequestInfo`: @@ -140,29 +171,46 @@ impl> IdCsrInner { /// Creates a new [IdCsrInner]. /// /// Fails, if [Name] or [Capabilities] do not meet polyproto validation criteria. + /// + /// The resulting `IdCsrInner` is guaranteed to be well-formed and up to polyproto specification, + /// if the correct [Target] for the CSRs intended usage context is provided. + /// + /// It is recommended to use [IdCsr::new] instead of this function, as it performs additional + /// validation and signing of the CSR. pub fn new( subject: &Name, public_key: &P, capabilities: &Capabilities, + target: Option, ) -> Result, ConversionError> { - subject.validate()?; - capabilities.validate()?; - let subject = subject.clone(); let subject_public_key_info = public_key.clone(); - - Ok(IdCsrInner { + let id_csr_inner = IdCsrInner { version: PkcsVersion::V1, subject, subject_public_key: subject_public_key_info, capabilities: capabilities.clone(), phantom_data: PhantomData, - }) + }; + id_csr_inner.validate(target)?; + Ok(id_csr_inner) + } + + /// Create an [IdCsrInner] from a byte slice containing a DER encoded PKCS #10 CSR. + /// The resulting `IdCsrInner` is guaranteed to be well-formed and up to polyproto specification, + /// if the correct [Target] for the CSRs intended usage context is provided. + pub fn from_der(bytes: &[u8], target: Option) -> Result { + let csr_inner = IdCsrInner::try_from(CertReqInfo::from_der(bytes)?)?; + csr_inner.validate(target)?; + Ok(csr_inner) } - /// Create an IdCsrInner from a byte slice containing a DER encoded PKCS #10 CSR. - pub fn from_der(bytes: &[u8]) -> Result { - IdCsrInner::try_from(CertReqInfo::from_der(bytes)?) + /// Create an unchecked [IdCsrInner] from a byte slice containing a DER encoded PKCS #10 CSR. + /// The caller is responsible for verifying the correctness of this `IdCsrInner` using + /// the [Constrained] trait before using it. + pub fn from_der_unchecked(bytes: &[u8]) -> Result { + let csr_inner = IdCsrInner::try_from(CertReqInfo::from_der(bytes)?)?; + Ok(csr_inner) } /// Encode this type as DER, returning a byte vector. @@ -174,11 +222,14 @@ impl> IdCsrInner { impl> TryFrom for IdCsr { type Error = ConversionError; + /// Tries to convert a `CertReq` into an `IdCsr`. The Ok() variant of this Result is an + /// unverified `IdCsr`. If this conversion is called manually, the caller is responsible for + /// verifying the `IdCsr` using the [Constrained] trait. fn try_from(value: CertReq) -> Result { Ok(IdCsr { inner_csr: IdCsrInner::try_from(value.info)?, signature_algorithm: value.algorithm, - signature: S::from_bitstring(value.signature.raw_bytes()), + signature: S::from_bytes(value.signature.raw_bytes()), }) } } @@ -186,14 +237,16 @@ impl> TryFrom for IdCsr { impl> TryFrom for IdCsrInner { type Error = ConversionError; + /// Tries to convert a `CertReqInfo` into an `IdCsrInner`. The Ok() variant of this Result is + /// an unverified `IdCsrInner`. If this conversion is called manually, the caller is responsible + /// for verifying the `IdCsrInner` using the [Constrained] trait. fn try_from(value: CertReqInfo) -> Result { let rdn_sequence = value.subject; - rdn_sequence.validate()?; + rdn_sequence.validate(None)?; let public_key_info = PublicKeyInfo { algorithm: value.public_key.algorithm, public_key_bitstring: value.public_key.subject_public_key, }; - Ok(IdCsrInner { version: PkcsVersion::V1, subject: rdn_sequence, diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 887fc16..800a11a 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -3,11 +3,16 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use std::ops::{Deref, DerefMut}; +use std::str::FromStr; -use der::asn1::{BitString, Ia5String}; +use der::asn1::BitString; +use der::pem::LineEnding; +use der::{Decode, DecodePem, Encode, EncodePem}; use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; -use x509_cert::name::Name; +use x509_cert::name::{Name, RdnSequence}; +use crate::errors::ConversionError; +use crate::types::der::asn1::Ia5String; use crate::{Constrained, ConstraintError, OID_RDN_DOMAIN_COMPONENT}; /// Additional capabilities ([x509_cert::ext::Extensions] or [x509_cert::attr::Attributes], depending @@ -46,16 +51,60 @@ impl DerefMut for SessionId { } } +impl std::fmt::Display for SessionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.session_id.fmt(f) + } +} + impl SessionId { #[allow(clippy::new_ret_no_self)] /// Creates a new [SessionId] which can be converted into an [Attribute] using `.as_attribute()`, /// if needed. Checks if the input is a valid Ia5String and if the [SessionId] constraints have /// been violated. - pub fn new_validated(id: Ia5String) -> Result { - let session_id = SessionId { session_id: id }; - session_id.validate()?; + pub fn new_validated(id: &str) -> Result { + let ia5string = match der::asn1::Ia5String::new(id) { + Ok(string) => string, + Err(_) => { + return Err(ConstraintError::Malformed(Some( + "Invalid Ia5String passed as SessionId".to_string(), + ))) + } + }; + + let session_id = SessionId { + session_id: ia5string.into(), + }; + session_id.validate(None)?; Ok(session_id) } + + /// Converts this [SessionId] into a [Name] for use in a certificate. + pub fn to_rdn_sequence(&self) -> Name { + RdnSequence::from_str(&format!("uniqueIdentifier={}", self)).unwrap() + } +} + +impl From for Ia5String { + fn from(value: SessionId) -> Self { + value.session_id + } +} + +impl TryFrom for SessionId { + type Error = ConstraintError; + + fn try_from(value: Ia5String) -> Result { + SessionId::new_validated(value.to_string().as_str()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// Whether something is intended for an actor or a home server. +#[allow(missing_docs)] +pub enum Target { + Actor, + HomeServer, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -76,7 +125,8 @@ pub enum PkcsVersion { V1 = 0, } -/// Information regarding a subjects' public key. +/// Information regarding a subjects' public key. This is a `SubjectPublicKeyInfo` in the context of +/// PKCS #10. #[derive(Debug, PartialEq, Eq, Clone)] pub struct PublicKeyInfo { /// Properties of the signature algorithm used to create the public key. @@ -85,6 +135,32 @@ pub struct PublicKeyInfo { pub public_key_bitstring: BitString, } +impl PublicKeyInfo { + /// Create a new [PublicKeyInfo] from the provided DER encoded data. The data must be a valid, + /// DER encoded PKCS #10 `SubjectPublicKeyInfo` structure. The caller is responsible for + /// verifying the correctness of the resulting data before using it. + pub fn from_der(value: &str) -> Result { + Ok(SubjectPublicKeyInfoOwned::from_der(value.as_bytes())?.into()) + } + + /// Create a new [PublicKeyInfo] from the provided PEM encoded data. The data must be a valid, + /// PEM encoded PKCS #10 `SubjectPublicKeyInfo` structure. The caller is responsible for + /// verifying the correctness of the resulting data before using it. + pub fn from_pem(value: &str) -> Result { + Ok(SubjectPublicKeyInfoOwned::from_pem(value.as_bytes())?.into()) + } + + /// Encode this type as DER, returning a byte vector. + pub fn to_der(&self) -> Result, ConversionError> { + Ok(SubjectPublicKeyInfoOwned::from(self.clone()).to_der()?) + } + + /// Encode this type as PEM, returning a string. + pub fn to_pem(&self, line_ending: LineEnding) -> Result { + Ok(SubjectPublicKeyInfoOwned::from(self.clone()).to_pem(line_ending)?) + } +} + impl From for PublicKeyInfo { fn from(value: SubjectPublicKeyInfoOwned) -> Self { PublicKeyInfo { @@ -108,15 +184,17 @@ impl From for SubjectPublicKeyInfoOwned { pub fn equal_domain_components(name_1: &Name, name_2: &Name) -> bool { let mut domain_components_1 = Vec::new(); let mut domain_components_2 = Vec::new(); - for (component_1, component_2) in name_1.0.iter().zip(name_2.0.iter()) { - for subcomponent_1 in component_1.0.iter() { - if subcomponent_1.oid.to_string().as_str() == OID_RDN_DOMAIN_COMPONENT { - domain_components_1.push(subcomponent_1); + for rdn in name_1.0.iter() { + for ava in rdn.0.iter() { + if ava.oid.to_string().as_str() == OID_RDN_DOMAIN_COMPONENT { + domain_components_1.push(String::from_utf8_lossy(ava.value.value())); } } - for subcomponent_2 in component_2.0.iter() { - if subcomponent_2.oid.to_string().as_str() == OID_RDN_DOMAIN_COMPONENT { - domain_components_2.push(subcomponent_2); + } + for rdn in name_2.0.iter() { + for ava in rdn.0.iter() { + if ava.oid.to_string().as_str() == OID_RDN_DOMAIN_COMPONENT { + domain_components_2.push(String::from_utf8_lossy(ava.value.value())); } } } diff --git a/src/constraints.rs b/src/constraints.rs deleted file mode 100644 index 4b79d42..0000000 --- a/src/constraints.rs +++ /dev/null @@ -1,390 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -use der::asn1::Ia5String; -use der::Length; -use regex::Regex; -use x509_cert::name::Name; - -use crate::certs::capabilities::{Capabilities, KeyUsage}; -use crate::certs::idcert::IdCert; -use crate::certs::idcerttbs::IdCertTbs; -use crate::certs::idcsr::IdCsr; -use crate::certs::{equal_domain_components, SessionId}; -use crate::errors::base::ConstraintError; -use crate::key::PublicKey; -use crate::signature::Signature; -use crate::{ - Constrained, OID_RDN_COMMON_NAME, OID_RDN_DOMAIN_COMPONENT, OID_RDN_UID, - OID_RDN_UNIQUE_IDENTIFIER, -}; - -impl Constrained for Name { - /// [Name] must meet the following criteria to be valid in the context of polyproto: - /// - Distinguished name MUST have "common name" attribute, which is equal to the actor or - /// home server name of the subject in question. Only one "common name" is allowed. - /// - MUST have AT LEAST one domain component, specifying the home server domain for this - /// entity. - /// - If actor name, MUST include UID (OID 0.9.2342.19200300.100.1.1) and uniqueIdentifier - /// (OID 0.9.2342.19200300.100.1.44). - /// - UID is the federation ID of the actor. - /// - uniqueIdentifier is the [SessionId] of the actor. - /// - MAY have "organizational unit" attributes - /// - MAY have other attributes, which might be ignored by other home servers and other clients. - fn validate(&self) -> Result<(), ConstraintError> { - let mut num_cn: u8 = 0; - let mut num_dc: u8 = 0; - let mut num_uid: u8 = 0; - let mut num_unique_identifier: u8 = 0; - - let rdns = &self.0; - for rdn in rdns.iter() { - for item in rdn.0.iter() { - match item.oid.to_string().as_str() { - OID_RDN_UID => { - num_uid += 1; - let fid_regex = - Regex::new(r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)") - .expect("Regex failed to compile"); - let string = String::from_utf8_lossy(item.value.value()).to_string(); - if !fid_regex.is_match(&string) { - return Err(ConstraintError::Malformed(Some( - "Provided Federation ID (FID) in uid field seems to be invalid" - .to_string(), - ))); - } - } - OID_RDN_UNIQUE_IDENTIFIER => { - num_unique_identifier += 1; - if let Ok(value) = - Ia5String::new(&String::from_utf8_lossy(item.value.value()).to_string()) - { - SessionId::new_validated(value)?; - } else { - return Err(ConstraintError::Malformed(Some( - "Tried to decode SessionID (uniqueIdentifier) as Ia5String and failed".to_string(), - ))); - } - } - OID_RDN_COMMON_NAME => { - num_cn += 1; - if num_cn > 1 { - return Err(ConstraintError::OutOfBounds { - lower: 1, - upper: 1, - actual: num_cn.to_string(), - reason: "Distinguished Names must include exactly one common name attribute.".to_string() - }); - } - } - OID_RDN_DOMAIN_COMPONENT => num_dc += 1, - _ => {} - } - } - } - if num_dc == 0 { - return Err(ConstraintError::OutOfBounds { - lower: 1, - upper: u8::MAX as i32, - actual: "0".to_string(), - reason: "Domain Component is missing".to_string(), - }); - } - if num_uid > 1 { - return Err(ConstraintError::OutOfBounds { - lower: 0, - upper: 1, - actual: num_uid.to_string(), - reason: "Too many UID components supplied".to_string(), - }); - } - if num_unique_identifier > 1 { - return Err(ConstraintError::OutOfBounds { - lower: 0, - upper: 1, - actual: num_unique_identifier.to_string(), - reason: "Too many uniqueIdentifier components supplied".to_string(), - }); - } - if num_unique_identifier > 0 && num_uid == 0 { - return Err(ConstraintError::OutOfBounds { - lower: 1, - upper: 1, - actual: num_uid.to_string(), - reason: "Actors must have uniqueIdentifier AND UID, only uniqueIdentifier found" - .to_string(), - }); - } - if num_uid > 0 && num_unique_identifier == 0 { - return Err(ConstraintError::OutOfBounds { - lower: 1, - upper: 1, - actual: num_unique_identifier.to_string(), - reason: "Actors must have uniqueIdentifier AND UID, only UID found".to_string(), - }); - } - Ok(()) - } -} - -impl Constrained for SessionId { - /// [SessionId] must be longer than 0 and not longer than 32 characters to be deemed valid. - fn validate(&self) -> Result<(), ConstraintError> { - if self.len() > Length::new(32) || self.len() == Length::ZERO { - return Err(ConstraintError::OutOfBounds { - lower: 1, - upper: 32, - actual: self.len().to_string(), - reason: "SessionId too long".to_string(), - }); - } - Ok(()) - } -} - -impl Constrained for Capabilities { - fn validate(&self) -> Result<(), ConstraintError> { - let is_ca = self.basic_constraints.ca; - - // Define the flags to check - let mut can_commit_content = false; - let mut can_sign = false; - let mut key_cert_sign = false; - let mut has_only_encipher = false; - let mut has_only_decipher = false; - let mut has_key_agreement = false; - - // Iterate over all the entries in the KeyUsage vector, check if they exist/are true - for item in self.key_usage.key_usages.iter() { - if !has_only_encipher && item == &KeyUsage::EncipherOnly { - has_only_encipher = true; - } - if !has_only_decipher && item == &KeyUsage::DecipherOnly { - has_only_decipher = true; - } - if !has_key_agreement && item == &KeyUsage::KeyAgreement { - has_key_agreement = true; - } - if !has_key_agreement && item == &KeyUsage::ContentCommitment { - can_commit_content = true; - } - if !has_key_agreement && item == &KeyUsage::DigitalSignature { - can_sign = true; - } - if !has_key_agreement && item == &KeyUsage::KeyCertSign { - key_cert_sign = true; - } - } - - // Non-CAs must be able to sign their messages. Whether with or without non-repudiation - // does not matter. - if !is_ca && !can_sign && !can_commit_content { - return Err(ConstraintError::Malformed(Some( - "Actors require signing capabilities, none found".to_string(), - ))); - } - - // Certificates cannot be both non-repudiating and repudiating - if can_sign && can_commit_content { - return Err(ConstraintError::Malformed(Some( - "Cannot have both signing and non-repudiation signing capabilities".to_string(), - ))); - } - - // If these Capabilities are for a CA, it also must have the KeyCertSign Capability set to - // true. Also, non-CAs are not allowed to have the KeyCertSign flag set to true. - if is_ca || key_cert_sign { - if !is_ca { - return Err(ConstraintError::Malformed(Some( - "If KeyCertSign capability is wanted, CA flag must be true".to_string(), - ))); - } - if !key_cert_sign { - return Err(ConstraintError::Malformed(Some( - "CA must have KeyCertSign capability".to_string(), - ))); - } - } - - // has_key_agreement needs to be true if has_only_encipher or _decipher are true. - // See: - // See: - if (has_only_encipher || has_only_decipher) && !has_key_agreement { - Err(ConstraintError::Malformed(Some( - "KeyAgreement capability needs to be true to use OnlyEncipher or OnlyDecipher" - .to_string(), - ))) - } else { - Ok(()) - } - } -} - -impl> Constrained for IdCsr { - fn validate(&self) -> Result<(), ConstraintError> { - self.inner_csr.capabilities.validate()?; - self.inner_csr.subject.validate()?; - match self.inner_csr.subject_public_key.verify_signature( - &self.signature, - match &self.inner_csr.clone().to_der() { - Ok(data) => data, - Err(_) => return Err(ConstraintError::Malformed(Some("DER conversion failure when converting inner IdCsr to DER. IdCsr is likely malformed".to_string()))) - } - ) { - Ok(_) => (), - Err(_) => return Err(ConstraintError::Malformed(Some("Provided signature does not match computed signature".to_string()))) - }; - Ok(()) - } -} - -impl> Constrained for IdCert { - fn validate(&self) -> Result<(), ConstraintError> { - self.id_cert_tbs.validate()?; - match self.id_cert_tbs.subject_public_key.verify_signature( - &self.signature, - match &self.id_cert_tbs.clone().to_der() { - Ok(data) => data, - Err(_) => { - return Err(ConstraintError::Malformed(Some( - "DER conversion failure when converting inner IdCertTbs to DER".to_string(), - ))); - } - }, - ) { - Ok(_) => Ok(()), - Err(_) => Err(ConstraintError::Malformed(Some( - "Provided signature does not match computed signature".to_string(), - ))), - } - } -} - -impl> Constrained for IdCertTbs { - fn validate(&self) -> Result<(), ConstraintError> { - self.capabilities.validate()?; - self.issuer.validate()?; - self.subject.validate()?; - match equal_domain_components(&self.issuer, &self.subject) { - true => (), - false => { - return Err(ConstraintError::Malformed(Some( - "Domain components of issuer and subject are not equal".to_string(), - ))) - } - } - Ok(()) - } -} - -#[cfg(test)] -mod name_constraints { - use std::str::FromStr; - - use x509_cert::name::Name; - - use crate::Constrained; - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn correct() { - let name = Name::from_str( - "cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=h3g2jt4dhfgj8hjs", - ) - .unwrap(); - name.validate().unwrap(); - let name = Name::from_str("CN=flori,DC=www,DC=polyphony,DC=chat").unwrap(); - name.validate().unwrap(); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn no_domain_component() { - let name = Name::from_str("CN=flori").unwrap(); - assert!(name.validate().is_err()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn two_cns() { - let name = Name::from_str("CN=flori,CN=xenia,DC=localhost").unwrap(); - assert!(name.validate().is_err()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn two_uid_or_uniqueid() { - let name = Name::from_str("CN=flori,CN=xenia,uid=numbaone,uid=numbatwo").unwrap(); - assert!(name.validate().is_err()); - let name = - Name::from_str("CN=flori,CN=xenia,uniqueIdentifier=numbaone,uniqueIdentifier=numbatwo") - .unwrap(); - assert!(name.validate().is_err()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn uid_and_no_uniqueid_or_uniqueid_and_no_uid() { - let name = Name::from_str("CN=flori,CN=xenia,uid=numbaone").unwrap(); - assert!(name.validate().is_err()); - let name = Name::from_str("CN=flori,CN=xenia,uniqueIdentifier=numbaone").unwrap(); - assert!(name.validate().is_err()) - } - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn malformed_session_id_fails() { - let name = - Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=").unwrap(); - assert!(name.validate().is_err()); - let name = - Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=123456789012345678901234567890123").unwrap(); - assert!(name.validate().is_err()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn malformed_uid_fails() { - let name = - Name::from_str("cn=flori,dc=localhost,uid=\"flori@\",uniqueIdentifier=3245").unwrap(); - assert!(name.validate().is_err()); - let name = - Name::from_str("cn=flori,dc=localhost,uid=\"flori@localhost\",uniqueIdentifier=3245") - .unwrap(); - assert!(name.validate().is_ok()); - let name = Name::from_str("cn=flori,dc=localhost,uid=\"1\",uniqueIdentifier=3245").unwrap(); - assert!(name.validate().is_err()); - } -} - -#[cfg(test)] -mod session_id_constraints { - - use der::asn1::Ia5String; - - use crate::certs::SessionId; - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn zero_long_session_id_fails() { - assert!(SessionId::new_validated(Ia5String::new("".as_bytes()).unwrap()).is_err()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn thirtytwo_length_session_id_is_ok() { - assert!(SessionId::new_validated( - Ia5String::new("11111111111111111111111111222222".as_bytes()).unwrap() - ) - .is_ok()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn thirtythree_length_session_id_fails() { - assert!(SessionId::new_validated( - Ia5String::new("111111111111111111111111112222223".as_bytes()).unwrap() - ) - .is_err()) - } -} diff --git a/src/constraints/capabilities.rs b/src/constraints/capabilities.rs new file mode 100644 index 0000000..2031a40 --- /dev/null +++ b/src/constraints/capabilities.rs @@ -0,0 +1,86 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::errors::{ERR_MSG_ACTOR_MISSING_SIGNING_CAPS, ERR_MSG_HOME_SERVER_MISSING_CA_ATTR}; + +use super::*; + +impl Constrained for Capabilities { + fn validate(&self, _target: Option) -> Result<(), ConstraintError> { + let is_ca = self.basic_constraints.ca; + + // Define the flags to check + let mut can_commit_content = false; + let mut can_sign = false; + let mut key_cert_sign = false; + let mut has_only_encipher = false; + let mut has_only_decipher = false; + let mut has_key_agreement = false; + + // Iterate over all the entries in the KeyUsage vector, check if they exist/are true + for item in self.key_usage.key_usages.iter() { + if !has_only_encipher && item == &KeyUsage::EncipherOnly { + has_only_encipher = true; + } + if !has_only_decipher && item == &KeyUsage::DecipherOnly { + has_only_decipher = true; + } + if !has_key_agreement && item == &KeyUsage::KeyAgreement { + has_key_agreement = true; + } + if !has_key_agreement && item == &KeyUsage::ContentCommitment { + can_commit_content = true; + } + if !has_key_agreement && item == &KeyUsage::DigitalSignature { + can_sign = true; + } + if !has_key_agreement && item == &KeyUsage::KeyCertSign { + key_cert_sign = true; + } + } + + // Non-CAs must be able to sign their messages. Whether with or without non-repudiation + // does not matter. + if !is_ca && !can_sign && !can_commit_content { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_ACTOR_MISSING_SIGNING_CAPS.to_string(), + ))); + } + + // Certificates cannot be both non-repudiating and repudiating + if can_sign && can_commit_content { + return Err(ConstraintError::Malformed(Some( + "Cannot have both signing and non-repudiation signing capabilities".to_string(), + ))); + } + + // If these Capabilities are for a CA, it also must have the KeyCertSign Capability set to + // true. Also, non-CAs are not allowed to have the KeyCertSign flag set to true. + if is_ca || key_cert_sign { + if !is_ca { + return Err(ConstraintError::Malformed(Some( + "If KeyCertSign capability is wanted, CA flag must be true".to_string(), + ))); + } + if !key_cert_sign { + return Err(ConstraintError::Malformed(Some(format!( + "{} Missing capability \"KeyCertSign\"", + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR + )))); + } + } + + // has_key_agreement needs to be true if has_only_encipher or _decipher are true. + // See: + // See: + if (has_only_encipher || has_only_decipher) && !has_key_agreement { + Err(ConstraintError::Malformed(Some( + "KeyAgreement capability needs to be true to use OnlyEncipher or OnlyDecipher" + .to_string(), + ))) + } else { + Ok(()) + } + } +} diff --git a/src/constraints/certs.rs b/src/constraints/certs.rs new file mode 100644 index 0000000..94b93e8 --- /dev/null +++ b/src/constraints/certs.rs @@ -0,0 +1,139 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use log::{debug, warn}; + +use crate::errors::{ + ERR_MSG_ACTOR_CANNOT_BE_CA, ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR, ERR_MSG_SIGNATURE_MISMATCH, +}; + +use super::*; + +impl> Constrained for IdCsrInner { + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + log::trace!( + "[IdCsrInner::validate()] validating capabilities for target: {:?}", + target + ); + self.capabilities.validate(target)?; + log::trace!( + "[IdCsrInner::validate()] validating subject for target: {:?}", + target + ); + self.subject.validate(target)?; + if let Some(target) = target { + match target { + Target::Actor => { + if self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_ACTOR_CANNOT_BE_CA.to_string(), + ))); + } + } + Target::HomeServer => { + if !self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR.to_string(), + ))); + } + } + } + } + Ok(()) + } +} + +impl> Constrained for IdCsr { + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + log::trace!( + "[IdCsr::validate()] validating inner CSR with target {:?}", + target + ); + self.inner_csr.validate(target)?; + log::trace!("[IdCsr::validate()] verifying signature"); + match self.inner_csr.subject_public_key.verify_signature( + &self.signature, + match &self.inner_csr.clone().to_der() { + Ok(data) => data, + Err(_) => { + log::warn!("[IdCsr::validate()] DER conversion failure when converting inner IdCsr to DER. IdCsr is likely malformed"); + return Err(ConstraintError::Malformed(Some("DER conversion failure when converting inner IdCsr to DER. IdCsr is likely malformed".to_string())))} + } + ) { + Ok(_) => (), + Err(_) => { + log::warn!( + "[IdCsr::validate()] {}", ERR_MSG_SIGNATURE_MISMATCH); + return Err(ConstraintError::Malformed(Some(ERR_MSG_SIGNATURE_MISMATCH.to_string())))} + }; + Ok(()) + } +} + +impl> Constrained for IdCert { + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + log::trace!( + "[IdCert::validate()] validating inner IdCertTbs with target {:?}", + target + ); + self.id_cert_tbs.validate(target)?; + Ok(()) + } +} + +impl> Constrained for IdCertTbs { + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + log::trace!( + "[IdCertTbs::validate()] validating capabilities for target: {:?}", + target + ); + self.capabilities.validate(target)?; + dbg!(self.issuer.to_string()); + self.issuer.validate(Some(Target::HomeServer))?; + self.subject.validate(target)?; + log::trace!( + "[IdCertTbs::validate()] checking if domain components of issuer and subject are equal" + ); + log::trace!( + "[IdCertTbs::validate()] Issuer: {}", + self.issuer.to_string() + ); + log::trace!( + "[IdCertTbs::validate()] Subject: {}", + self.subject.to_string() + ); + match equal_domain_components(&self.issuer, &self.subject) { + true => debug!("Domain components of issuer and subject are equal"), + false => { + warn!( + "{}\nIssuer: {}\nSubject: {}", + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, &self.issuer, &self.subject + ); + return Err(ConstraintError::Malformed(Some( + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT.to_string(), + ))); + } + } + if let Some(target) = target { + match target { + Target::Actor => { + if self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_ACTOR_CANNOT_BE_CA.to_string(), + ))); + } + } + Target::HomeServer => { + if !self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR.to_string(), + ))); + } + } + } + } + Ok(()) + } +} diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs new file mode 100644 index 0000000..ffe33b4 --- /dev/null +++ b/src/constraints/mod.rs @@ -0,0 +1,213 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use der::Length; +use regex::Regex; +use x509_cert::name::{Name, RelativeDistinguishedName}; + +use crate::certs::capabilities::{Capabilities, KeyUsage}; +use crate::certs::idcert::IdCert; +use crate::certs::idcerttbs::IdCertTbs; +use crate::certs::idcsr::{IdCsr, IdCsrInner}; +use crate::certs::{equal_domain_components, SessionId, Target}; +use crate::errors::ConstraintError; +use crate::key::PublicKey; +use crate::signature::Signature; +use crate::{ + Constrained, OID_RDN_COMMON_NAME, OID_RDN_DOMAIN_COMPONENT, OID_RDN_UID, + OID_RDN_UNIQUE_IDENTIFIER, +}; + +mod capabilities; +mod certs; +mod name; +mod session_id; +#[cfg(feature = "types")] +mod types; + +#[cfg(test)] +mod name_constraints { + use std::str::FromStr; + + use x509_cert::name::Name; + + use crate::certs::Target; + use crate::testing_utils::init_logger; + use crate::Constrained; + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn correct() { + init_logger(); + let name = Name::from_str( + "cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=h3g2jt4dhfgj8hjs", + ) + .unwrap(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + name.validate(target).unwrap(); + let name = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=meow", + ) + .unwrap(); + name.validate(target).unwrap(); + let name = Name::from_str( + "cn=flori,dc=some,dc=domain,dc=that,dc=is,dc=quite,dc=long,dc=geez,dc=thats,dc=alotta,dc=subdomains,dc=example,dc=com,uid=flori@some.domain.that.is.quite.long.geez.thats.alotta.subdomains.example.com,uniqueIdentifier=h3g2jt4dhfgj8hjs", + ) + .unwrap(); + name.validate(target).unwrap(); + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn mismatch_uid_dcs() { + init_logger(); + let targets = [None, Some(Target::Actor), Some(Target::HomeServer)]; + for target in targets.into_iter() { + let name = Name::from_str( + "cn=flori,dc=some,dc=domain,dc=that,dc=is,dc=quite,dc=long,dc=geez,dc=alotta,dc=subdomains,dc=example,dc=com,uid=flori@some.domain.that.is.quite.long.geez.thats.alotta.subdomains.example.com,uniqueIdentifier=h3g2jt4dhfgj8hjs", + ) + .unwrap(); + name.validate(target).err().unwrap(); + + let name = Name::from_str( + "cn=flori,dc=some,dc=domain,dc=that,dc=is,dc=quite,dc=long,dc=geez,dc=alotta,dc=subdomains,dc=example,dc=com,uid=flori@domain.that.is.quite.long.geez.thats.alotta.subdomains.example.com,uniqueIdentifier=h3g2jt4dhfgj8hjs", + ) + .unwrap(); + name.validate(target).err().unwrap(); + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn no_domain_component() { + init_logger(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + let name = Name::from_str("CN=flori").unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str("CN=flori,uid=flori@localhost").unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str("CN=flori,uniqueIdentifier=12345678901234567890123456789012") + .unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str( + "CN=flori,uid=flori@localhost,uniqueIdentifier=12345678901234567890123456789012", + ) + .unwrap(); + assert!(name.validate(target).is_err()); + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn two_cns() { + init_logger(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + let name = Name::from_str("CN=flori,CN=xenia,DC=localhost").unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str("CN=flori,CN=xenia,uid=numbaone").unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str("CN=flori,CN=xenia,uniqueIdentifier=numbaone").unwrap(); + assert!(name.validate(target).is_err()); + let name = + Name::from_str("CN=flori,CN=xenia,uid=numbaone,uniqueIdentifier=numbatwo").unwrap(); + assert!(name.validate(target).is_err()); + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn two_uid_or_uniqueid() { + init_logger(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + let name = Name::from_str("CN=flori,DC=localhost,uid=numbaone,uid=numbatwo").unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str( + "CN=flori,DC=localhost,uniqueIdentifier=numbaone,uniqueIdentifier=numbatwo", + ) + .unwrap(); + assert!(name.validate(target).is_err()); + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn uid_and_no_uniqueid_or_uniqueid_and_no_uid() { + init_logger(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + let name = Name::from_str("CN=flori,CN=xenia,uid=numbaone").unwrap(); + assert!(name.validate(target).is_err()); + let name = Name::from_str("CN=flori,CN=xenia,uniqueIdentifier=numbaone").unwrap(); + assert!(name.validate(target).is_err()) + } + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn malformed_session_id_fails() { + init_logger(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + let name = + Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=") + .unwrap(); + assert!(name.validate(target).is_err()); + let name = + Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=123456789012345678901234567890123").unwrap(); + assert!(name.validate(target).is_err()); + let name = + Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=变性人的生命和权利必须得到保护").unwrap(); + assert!(name.validate(target).is_err()); + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn malformed_uid_fails() { + init_logger(); + let targets = [None, Some(Target::Actor)]; + for target in targets.into_iter() { + let name = + Name::from_str("cn=flori,dc=localhost,uid=flori@,uniqueIdentifier=3245").unwrap(); + assert!(name.validate(target).is_err()); + let name = + Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=3245") + .unwrap(); + assert!(name.validate(target).is_ok()); + let name = Name::from_str("cn=flori,dc=localhost,uid=1,uniqueIdentifier=3245").unwrap(); + assert!(name.validate(target).is_err()); + let name = + Name::from_str("cn=flori,dc=localhost,uid=变性人的生命和权利必须得到保护@localhost,uniqueIdentifier=3245").unwrap(); + assert!(name.validate(target).is_err()); + } + } +} + +#[cfg(test)] +mod session_id_constraints { + + use crate::certs::SessionId; + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn zero_long_session_id_fails() { + assert!(SessionId::new_validated("").is_err()) + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn thirtytwo_length_session_id_is_ok() { + assert!(SessionId::new_validated("11111111111111111111111111222222").is_ok()) + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn thirtythree_length_session_id_fails() { + assert!(SessionId::new_validated("111111111111111111111111112222223").is_err()) + } +} diff --git a/src/constraints/name.rs b/src/constraints/name.rs new file mode 100644 index 0000000..dae9e54 --- /dev/null +++ b/src/constraints/name.rs @@ -0,0 +1,344 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::errors::ERR_MSG_DC_UID_MISMATCH; + +use x509_cert::attr::AttributeTypeAndValue; + +use super::*; + +impl Constrained for Name { + /// [Name] must meet the following criteria to be valid in the context of polyproto: + /// - Distinguished name MUST have "common name" attribute, which is equal to the actor or + /// home server name of the subject in question. Only one "common name" is allowed. + /// - MUST have AT LEAST one domain component, specifying the home server domain for this + /// entity. + /// - If actor name, MUST include UID (OID 0.9.2342.19200300.100.1.1) and uniqueIdentifier + /// (OID 0.9.2342.19200300.100.1.44). + /// - UID is the federation ID of the actor. + /// - uniqueIdentifier is the [SessionId] of the actor. + /// - MAY have "organizational unit" attributes + /// - MAY have other attributes, which might be ignored by other home servers and other clients. + // I apologize. This is horrible. I'll redo it eventually. Depression made me do it. -bitfl0wer + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + log::trace!("[Name::validate()] Validating Name: {}", self.to_string()); + let mut num_cn: u8 = 0; + let mut num_dc: u8 = 0; + let mut num_uid: u8 = 0; + let mut num_unique_identifier: u8 = 0; + let mut vec_dc: Vec = Vec::new(); + let mut uid: RelativeDistinguishedName = RelativeDistinguishedName::default(); + let mut cn: RelativeDistinguishedName = RelativeDistinguishedName::default(); + + let rdns = &self.0; + for rdn in rdns.iter() { + log::trace!( + "[Name::validate()] Determining OID of RDN {} and performing appropriate validation", + rdn.to_string() + ); + for item in rdn.0.iter() { + match item.oid.to_string().as_str() { + OID_RDN_UID => { + log::trace!("[Name::validate()] Found UID in RDN: {}", item.to_string()); + num_uid += 1; + uid = rdn.clone(); + validate_rdn_uid(item)?; + } + OID_RDN_UNIQUE_IDENTIFIER => { + log::trace!( + "[Name::validate()] Found uniqueIdentifier in RDN: {}", + item.to_string() + ); + num_unique_identifier += 1; + validate_rdn_unique_identifier(item)?; + } + OID_RDN_COMMON_NAME => { + log::trace!( + "[Name::validate()] Found Common Name in RDN: {}", + item.to_string() + ); + num_cn += 1; + cn = rdn.clone(); + if num_cn > 1 { + return Err(ConstraintError::OutOfBounds { + lower: 1, + upper: 1, + actual: num_cn.to_string(), + reason: "[Name::validate()] Distinguished Names must not contain more than one Common Name field".to_string() + }); + } + } + OID_RDN_DOMAIN_COMPONENT => { + log::trace!( + "[Name::validate()] Found Domain Component in RDN: {}", + item.to_string() + ); + num_dc += 1; + vec_dc.push(rdn.clone()); + } + _ => { + log::trace!( + "[Name::validate()] Found unknown/non-validated component in RDN: {}", + item.to_string() + ); + } + } + } + } + // The order of the DCs is reversed in the [Name] object, compared to the order of the DCs in the UID. + vec_dc.reverse(); + if let Some(target) = target { + match target { + Target::Actor => { + log::trace!( + "[Name::validate()] Validating DC {:?} matches DC in UID {}", + vec_dc + .iter() + .map(|dc| dc.to_string()) + .collect::>(), + uid.to_string() + ); + validate_dc_matches_dc_in_uid(&vec_dc, &uid)?; + } + Target::HomeServer => { + if num_uid > 0 || num_unique_identifier > 0 { + return Err(ConstraintError::OutOfBounds { + lower: 0, + upper: 0, + actual: "1".to_string(), + reason: "Home Servers must not have UID or uniqueIdentifier" + .to_string(), + }); + } + } + }; + } else if num_uid != 0 { + validate_dc_matches_dc_in_uid(&vec_dc, &uid)?; + } + log::trace!( + "Encountered {} UID components and {} Common Name components", + num_uid, + num_cn + ); + if num_uid != 0 && num_cn != 0 { + log::trace!("Validating UID username matches Common Name"); + validate_uid_username_matches_cn(&uid, &cn)?; + } + if num_dc == 0 { + return Err(ConstraintError::OutOfBounds { + lower: 1, + upper: u8::MAX as i32, + actual: "0".to_string(), + reason: "Domain Component is missing in Name component".to_string(), + }); + } + if num_uid > 1 { + return Err(ConstraintError::OutOfBounds { + lower: 0, + upper: 1, + actual: num_uid.to_string(), + reason: "Too many UID components supplied".to_string(), + }); + } + if num_unique_identifier > 1 { + return Err(ConstraintError::OutOfBounds { + lower: 0, + upper: 1, + actual: num_unique_identifier.to_string(), + reason: "Too many uniqueIdentifier components supplied".to_string(), + }); + } + if num_unique_identifier > 0 && num_uid == 0 { + return Err(ConstraintError::OutOfBounds { + lower: 1, + upper: 1, + actual: num_uid.to_string(), + reason: "Actors must have uniqueIdentifier AND UID, only uniqueIdentifier found" + .to_string(), + }); + } + if num_uid > 0 && num_unique_identifier == 0 { + return Err(ConstraintError::OutOfBounds { + lower: 1, + upper: 1, + actual: num_unique_identifier.to_string(), + reason: "Actors must have uniqueIdentifier AND UID, only UID found".to_string(), + }); + } + Ok(()) + } +} + +/// Check if the domain components are equal between the UID and the DCs +fn validate_dc_matches_dc_in_uid( + vec_dc: &[RelativeDistinguishedName], + uid: &RelativeDistinguishedName, +) -> Result<(), ConstraintError> { + // Find the position of the @ in the UID + let position_of_at = match uid.to_string().find('@') { + Some(pos) => pos, + None => { + log::warn!( + "[validate_dc_matches_dc_in_uid] UID {} does not contain an @", + uid.to_string() + ); + return Err(ConstraintError::Malformed(Some( + "UID does not contain an @".to_string(), + ))); + } + }; + // Split the UID at the @ + let uid_without_username = uid.to_string().split_at(position_of_at + 1).1.to_string(); // +1 to not include the @ + let dc_normalized_uid: Vec<&str> = uid_without_username.split('.').collect(); + dbg!(dc_normalized_uid.clone()); + let mut index = 0u8; + // Iterate over the DCs in the UID and check if they are equal to the DCs in the DCs + for component in dc_normalized_uid.iter() { + let equivalent_dc = match vec_dc.get(index as usize) { + Some(dc) => dc, + None => { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_DC_UID_MISMATCH.to_string(), + ))) + } + }; + let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); + if component != &equivalent_dc.to_string() { + return Err(ConstraintError::Malformed(Some( + ERR_MSG_DC_UID_MISMATCH.to_string(), + ))); + } + index = match index.checked_add(1) { + Some(i) => i, + None => { + return Err(ConstraintError::Malformed(Some( + "More than 255 Domain Components found".to_string(), + ))) + } + }; + } + Ok(()) +} + +/// Validate the UID field in the RDN. This performs a regex check to see if the UID is a valid +/// Federation ID (FID). +fn validate_rdn_uid(item: &AttributeTypeAndValue) -> Result<(), ConstraintError> { + let fid_regex = Regex::new(r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)") + .expect("Regex failed to compile"); + let string = String::from_utf8_lossy(item.value.value()).to_string(); + if !fid_regex.is_match(&string) { + Err(ConstraintError::Malformed(Some( + "Provided Federation ID (FID) in uid field seems to be invalid".to_string(), + ))) + } else { + Ok(()) + } +} + +/// Validate the uniqueIdentifier field in the RDN. This performs a check to see if the provided +/// input is a valid [SessionId]. +fn validate_rdn_unique_identifier(item: &AttributeTypeAndValue) -> Result<(), ConstraintError> { + SessionId::new_validated(&String::from_utf8_lossy(item.value.value()))?; + Ok(()) +} + +/// Validate that the UID username matches the Common Name +fn validate_uid_username_matches_cn( + uid: &RelativeDistinguishedName, + cn: &RelativeDistinguishedName, +) -> Result<(), ConstraintError> { + // Find the position of the @ in the UID + let uid_str = uid.to_string().split_off(4); + let cn_str = cn.to_string().split_off(3); + let position_of_at = match uid_str.find('@') { + Some(pos) => pos, + None => { + log::warn!( + "[validate_dc_matches_dc_in_uid] UID \"{}\" does not contain an @", + uid.to_string() + ); + return Err(ConstraintError::Malformed(Some( + "UID does not contain an @".to_string(), + ))); + } + }; + // Split the UID at the @ + let uid_username_only = uid_str.to_string().split_at(position_of_at).0.to_string(); + match uid_username_only == cn_str { + true => Ok(()), + false => { + log::warn!( + "[validate_uid_username_matches_cn] UID username \"{}\" does not match the Common Name \"{}\"", + uid_username_only, + cn_str + ); + Err(ConstraintError::Malformed(Some( + "UID username does not match the Common Name".to_string(), + ))) + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::testing_utils::init_logger; + + use super::*; + + #[test] + fn test_dc_matches_dc_in_uid() { + let good_name = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(); + let bad_name = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphonyy.chat,uniqueIdentifier=client1", + ) + .unwrap(); + assert!(good_name.validate(Some(Target::Actor)).is_ok()); + assert!(bad_name.validate(Some(Target::Actor)).is_err()); + let bad_name = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.cat,uniqueIdentifier=client1", + ) + .unwrap(); + assert!(bad_name.validate(Some(Target::Actor)).is_err()); + assert!(bad_name.validate(Some(Target::Actor)).is_err()); + let bad_name = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@thisis.polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(); + assert!(bad_name.validate(Some(Target::Actor)).is_err()); + } + + #[test] + fn cn_has_to_match_uid_name() { + init_logger(); + let cn = Name::from_str("cn=bitfl0wer").unwrap(); + let uid = Name::from_str("uid=flori@localhost").unwrap(); + assert!( + validate_uid_username_matches_cn(uid.0.first().unwrap(), cn.0.first().unwrap()) + .is_err() + ); + let cn = Name::from_str("cn=flori").unwrap(); + assert!( + validate_uid_username_matches_cn(uid.0.first().unwrap(), cn.0.first().unwrap()).is_ok() + ); + let good_name = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(); + let bad_name = Name::from_str( + "CN=bitfl0wer,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(); + assert!(good_name.validate(None).is_ok()); + assert!(bad_name.validate(None).is_err()); + assert!(bad_name.validate(Some(Target::Actor)).is_err()); + assert!(bad_name.validate(Some(Target::HomeServer)).is_err()); + assert!(good_name.validate(Some(Target::Actor)).is_ok()); + assert!(good_name.validate(Some(Target::HomeServer)).is_err()); + } +} diff --git a/src/constraints/session_id.rs b/src/constraints/session_id.rs new file mode 100644 index 0000000..79ac9bc --- /dev/null +++ b/src/constraints/session_id.rs @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use super::*; + +impl Constrained for SessionId { + /// [SessionId] must be longer than 0 and not longer than 32 characters to be deemed valid. + fn validate(&self, _target: Option) -> Result<(), ConstraintError> { + if self.len() > Length::new(32) || self.len() == Length::ZERO { + return Err(ConstraintError::OutOfBounds { + lower: 1, + upper: 32, + actual: self.len().to_string(), + reason: "SessionId too long".to_string(), + }); + } + Ok(()) + } +} diff --git a/src/constraints/types/challenge_string.rs b/src/constraints/types/challenge_string.rs new file mode 100644 index 0000000..0f476d9 --- /dev/null +++ b/src/constraints/types/challenge_string.rs @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::errors::ERR_MSG_CHALLENGE_STRING_LENGTH; +use crate::types::ChallengeString; + +use super::*; + +impl Constrained for ChallengeString { + fn validate(&self, _target: Option) -> Result<(), ConstraintError> { + if self.challenge.len() < 32 || self.challenge.len() > 255 { + return Err(ConstraintError::OutOfBounds { + lower: 32, + upper: 255, + actual: self.challenge.len().to_string(), + reason: ERR_MSG_CHALLENGE_STRING_LENGTH.to_string(), + }); + } + Ok(()) + } +} diff --git a/src/constraints/types/federation_id.rs b/src/constraints/types/federation_id.rs new file mode 100644 index 0000000..e9a07c9 --- /dev/null +++ b/src/constraints/types/federation_id.rs @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use regex::Regex; + +use crate::errors::ERR_MSG_FEDERATION_ID_REGEX; +use crate::types::{FederationId, REGEX_FEDERATION_ID}; + +use super::*; + +impl Constrained for FederationId { + fn validate(&self, _target: Option) -> Result<(), ConstraintError> { + let fid_regex = Regex::new(REGEX_FEDERATION_ID).unwrap(); + match fid_regex.is_match(&self.inner) { + true => Ok(()), + false => Err(ConstraintError::Malformed(Some( + ERR_MSG_FEDERATION_ID_REGEX.to_string(), + ))), + } + } +} diff --git a/src/constraints/types/mod.rs b/src/constraints/types/mod.rs new file mode 100644 index 0000000..92b86ea --- /dev/null +++ b/src/constraints/types/mod.rs @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod challenge_string; +mod federation_id; + +use crate::certs::Target; +use crate::errors::ConstraintError; +use crate::Constrained; diff --git a/src/errors/base.rs b/src/errors/base.rs index 2eff2eb..89d80c9 100644 --- a/src/errors/base.rs +++ b/src/errors/base.rs @@ -5,27 +5,40 @@ use thiserror::Error; #[derive(Error, Debug, PartialEq, Clone)] +/// Constraint validation errors. pub enum ConstraintError { #[error("The value did not meet the set validation criteria and is considered malformed")] + /// The value did not meet the set validation criteria and is considered malformed Malformed(Option), #[error("The value was expected to be between {lower:?} and {upper:?} but was {actual:?}")] + /// A value is out of bounds OutOfBounds { + /// The lower bound of the value lower: i32, + /// The upper bound of the value upper: i32, + /// The actual value actual: String, + /// Additional context reason: String, }, } -/// Represents errors for invalid input in IdCsr or IdCert generation. +/// Represents errors for invalid input. Differs from [ConstraintError], in that `ConstraintError` is +/// only used on types implementing the [crate::Constrained] trait. #[derive(Error, Debug, PartialEq, Clone)] pub enum InvalidInput { #[error("The value is malformed and cannot be used as input: {0}")] + /// The value is malformed and cannot be used as input Malformed(String), #[error("The value was expected to be between {min_length:?} and {max_length:?} but was {actual_length:?}")] + /// A value is out of bounds Length { + /// The minimum length of the value min_length: usize, + /// The maximum length of the value max_length: usize, + /// The actual length of the value actual_length: String, }, } diff --git a/src/errors/composite.rs b/src/errors/composite.rs index 9172408..d18c490 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -8,41 +8,71 @@ use thiserror::Error; use super::base::{ConstraintError, InvalidInput}; #[derive(Error, Debug, PartialEq, Clone)] +/// Errors that can occur when validating a certificate pub enum InvalidCert { - #[error("The signature does not match the contents of the certificate")] - InvalidSignature, - #[error("The subject presented on the certificate is malformed or otherwise invalid")] - InvalidSubject(ConstraintError), - #[error("The issuer presented on the certificate is malformed or otherwise invalid")] - InvalidIssuer(ConstraintError), + #[error(transparent)] + /// Signature or public key are invalid + PublicKeyError(#[from] PublicKeyError), + #[error(transparent)] + /// The certificate does not pass validation of polyproto constraints + InvalidProperties(#[from] ConstraintError), #[error("The validity period of the certificate is invalid, or the certificate is expired")] + /// The certificate is expired or has an invalid validity period InvalidValidity, - #[error("The capabilities presented on the certificate are invalid or otherwise malformed")] - InvalidCapabilities(ConstraintError), } -#[derive(Error, Debug, PartialEq, Hash, Clone)] +#[derive(Error, Debug, PartialEq, Hash, Clone, Copy)] +/// Errors related to Public Keys and Signatures pub enum PublicKeyError { #[error("The signature does not match the data")] + /// The signature does not match the data BadSignature, #[error("The provided PublicKeyInfo could not be made into a PublicKey")] + /// The provided PublicKey is invalid BadPublicKeyInfo, } #[derive(Error, Debug, PartialEq, Clone)] +/// Errors that can occur when converting between types pub enum ConversionError { #[error(transparent)] + /// The constraints of the source or target types were met ConstraintError(#[from] ConstraintError), #[error(transparent)] + /// The input was invalid - Either malformed or out of bounds InvalidInput(#[from] InvalidInput), #[error("Encountered DER encoding error")] + /// An error occurred while parsing a DER encoded object DerError(der::Error), #[error("Encountered DER OID error")] + /// An error occurred while parsing an OID ConstOidError(der::oid::Error), #[error("Critical extension cannot be converted")] - UnknownCriticalExtension { oid: ObjectIdentifier }, + /// A critical extension is unknown and cannot be converted + UnknownCriticalExtension { + /// The OID of the unknown extension + oid: ObjectIdentifier, + }, + #[error(transparent)] + /// The source or target certificate is invalid + InvalidCert(#[from] InvalidCert), +} +#[cfg(feature = "reqwest")] +#[derive(Error, Debug)] +/// Errors that can occur when making a request +pub enum RequestError { + #[error(transparent)] + /// Reqwest encountered an error + HttpError(#[from] reqwest::Error), + #[error("Failed to deserialize response into expected type")] + /// The response could not be deserialized into the expected type + DeserializationError(#[from] serde_json::Error), + #[error("Failed to convert response into expected type")] + /// The response could not be converted into the expected type + ConversionError(#[from] ConversionError), #[error(transparent)] - IdCertError(#[from] PublicKeyError), + /// The URL could not be parsed + UrlError(#[from] url::ParseError), } impl From for ConversionError { diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 8edc686..63675d2 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -2,11 +2,32 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#![allow(missing_docs)] + +pub static ERR_MSG_HOME_SERVER_MISSING_CA_ATTR: &str = + "Home servers CSRs and Certificates must have the \"CA\" capability set to true!"; +pub static ERR_MSG_ACTOR_CANNOT_BE_CA: &str = + "Actor CSRs and Certificates must not have \"CA\" capabilities!"; +pub static ERR_MSG_SIGNATURE_MISMATCH: &str = + "Provided signature does not match computed signature!"; +pub static ERR_MSG_ACTOR_MISSING_SIGNING_CAPS: &str = + "Actors require one of the following capabilities: \"DigitalSignature\", \"ContentCommitment\". None provided."; +pub static ERR_MSG_DC_UID_MISMATCH: &str = + "The domain components found in the DC and UID fields of the Name object do not match!"; +pub static ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT: &str = + "The domain components of the issuer and the subject do not match!"; +pub static ERR_CERTIFICATE_TO_DER_ERROR: &str = + "The certificate seems to be malformed, as it cannot be converted to DER."; +#[cfg(feature = "types")] +pub static ERR_MSG_CHALLENGE_STRING_LENGTH: &str = + "Challenge strings must be between 32 and 255 bytes long!"; +#[cfg(feature = "types")] +pub static ERR_MSG_FEDERATION_ID_REGEX: &str = + "Federation IDs must match the regex: \\b([a-z0-9._%+-]+)@([a-z0-9-]+(\\.[a-z0-9-]+)*)"; /// "Base" error types which can be combined into "composite" error types pub mod base; /// "Composite" error types which consist of one or more "base" error types pub mod composite; -// PRETTYFYME -// This module can be restructured to be a reflection of the src/ file tree. It would then be very -// easy to tell, which file covers error types of which data types +pub use base::*; +pub use composite::*; diff --git a/src/key.rs b/src/key.rs index f4315fd..546cbcb 100644 --- a/src/key.rs +++ b/src/key.rs @@ -5,12 +5,13 @@ use spki::AlgorithmIdentifierOwned; use crate::certs::PublicKeyInfo; -use crate::errors::composite::{ConversionError, PublicKeyError}; +use crate::errors::{ConversionError, PublicKeyError}; use crate::signature::Signature; /// A cryptographic private key generated by a [AlgorithmIdentifierOwned], with /// a corresponding [PublicKey] pub trait PrivateKey: PartialEq + Eq { + /// The public key type corresponding to this private key. type PublicKey: PublicKey; /// Returns the public key corresponding to this private key. fn pubkey(&self) -> &Self::PublicKey; diff --git a/src/lib.rs b/src/lib.rs index b5118f8..d51c01d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,21 +3,32 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. /*! +

+ +[![Discord]][Discord-invite] +[![Matrix]][Matrix-invite] +[![Build][build-shield]][build-url] +[![Coverage][coverage-shield]][coverage-url] +Blue status badge, reading 'Alpha' + +
# polyproto -(Generic) Rust types and traits to quickly get a +Crate supplying (generic) Rust types and traits to quickly get a [polyproto](https://docs.polyphony.chat/Protocol%20Specifications/core/) implementation up and -running. +running, as well as an HTTP client for the polyproto API. -## Implementing polyproto +Building upon types offered by the [der](https://crates.io/crates/der), +[x509_cert](https://crates.io/crates/x509_cert) and [spki](https://crates.io/crates/spki) crates, +this crate provides a set of types and traits to quickly implement the polyproto specification. +Simply add cryptography and signature algorithm crates of your choice to the mix, and you are ready +to go. -**The crate is currently in very early (alpha) development. A lot of functionality is missing, and -things may break or change at any point in time.** +All polyproto certificate types can be converted to and from the types offered by the `x509_cert` +crate. -This crate extends upon types offered by [der](https://crates.io/crates/der) and -[spki](https://crates.io/crates/spki). As such, these crates are required dependencies for -projects looking to implement polyproto. +## Implementing polyproto Start by implementing the trait [crate::signature::Signature] for a signature algorithm of your choice. Popular crates for cryptography and signature algorithms supply their own `PublicKey` and @@ -27,6 +38,9 @@ choice. Popular crates for cryptography and signature algorithms supply their ow You can then use the [crate::certs] types to build certificates using your implementations of the aforementioned traits. +**View the [examples](./examples/)** directory for a simple example on how to implement and use this +crate with the ED25519 signature algorithm. + ## Cryptography This crate provides no cryptographic functionality whatsoever; its sole purpose is to aid in @@ -34,6 +48,15 @@ implementing polyproto by transforming the [polyproto specification](https://docs.polyphony.chat/Protocol%20Specifications/core/) into well-defined yet adaptable Rust types. +## Safety + +Please refer to the documentation of individual functions for information on which safety guarantees +they provide. Methods returning certificates, certificate requests and other types where the +validity and correctness of the data has a chance of impacting the security of a system always +mention the safety guarantees they provide in their respective documentation. + +This crate has not undergone any security audits. + ## WebAssembly This crate is designed to work with the `wasm32-unknown-unknown` target. To compile for `wasm`, you @@ -44,61 +67,117 @@ will have to use the `wasm` feature: polyproto = { version = "0", features = ["wasm"] } ``` +## HTTP API client through `reqwest` + +If the `reqwest` feature is activated, this crate offers a polyproto HTTP API client, using the +`reqwest` crate. + +### Alternatives to `reqwest` + +If you would like to implement an HTTP client using something other than `reqwest`, simply enable +the `types` and `serde` features. Using these features, you can implement your own HTTP client, with +the polyproto crate acting as a single source of truth for request and response types, as well as +request routes and methods through the exported `static` `Route`s. + +[build-shield]: https://img.shields.io/github/actions/workflow/status/polyphony-chat/polyproto/build_and_test.yml?style=flat +[build-url]: https://github.com/polyphony-chat/polyproto/blob/main/.github/workflows/build_and_test.yml +[coverage-shield]: https://coveralls.io/repos/github/polyphony-chat/polyproto/badge.svg?branch=main +[coverage-url]: https://coveralls.io/github/polyphony-chat/polyproto?branch=main +[Discord]: https://dcbadge.vercel.app/api/server/m3FpcapGDD?style=flat +[Discord-invite]: https://discord.com/invite/m3FpcapGDD +[Matrix]: https://img.shields.io/matrix/polyproto%3Atu-dresden.de?server_fqdn=matrix.org&style=flat&label=Matrix%20Room +[Matrix-invite]: https://matrix.to/#/#polyproto:tu-dresden.de */ +#![forbid(unsafe_code)] +#![warn( + missing_docs, + missing_debug_implementations, + missing_copy_implementations +)] + +/// The OID for the `domainComponent` RDN pub const OID_RDN_DOMAIN_COMPONENT: &str = "0.9.2342.19200300.100.1.25"; +/// The OID for the `commonName` RDN pub const OID_RDN_COMMON_NAME: &str = "2.5.4.3"; +/// The OID for the `uniqueIdentifier` RDN pub const OID_RDN_UNIQUE_IDENTIFIER: &str = "0.9.2342.19200300.100.1.44"; +/// The OID for the `uid` RDN pub const OID_RDN_UID: &str = "0.9.2342.19200300.100.1.1"; +use certs::Target; use errors::base::ConstraintError; -#[warn( - missing_docs, - missing_debug_implementations, - missing_copy_implementations, - clippy::unnecessary_mut_passed -)] -#[deny(clippy::unwrap_used, clippy::todo, clippy::unimplemented)] -#[forbid(unsafe_code)] - +#[cfg(feature = "reqwest")] +/// Ready-to-use API routes, implemented using `reqwest` +pub mod api; /// Generic polyproto certificate types and traits. pub mod certs; +/// Error types used in this crate +pub mod errors; /// Generic polyproto public- and private key traits. pub mod key; /// Generic polyproto signature traits. pub mod signature; +#[cfg(feature = "types")] +/// Types used in polyproto and the polyproto HTTP/REST APIs +pub mod types; -/// Error types used in this crate -pub mod errors; - -pub(crate) mod constraints; +mod constraints; pub use der; pub use spki; +pub use x509_cert::name::*; -/// Traits implementing [Constrained] can be validated to be well-formed. This does not guarantee -/// that a validated type will always be *correct* in the context it is in. +/// Types implementing [Constrained] can be validated to be well-formed. +/// +/// ## `Target` parameter +/// +/// The `target` parameter is used to specify the context in which the type should be validated. +/// For example: Specifying a [Target] of `Actor` would also check that the IdCert is not a CA +/// certificate, among other things. +/// +/// If the `target` is `None`, the type will be validated without +/// considering this context. If you know the context in which the type will be used, there is no +/// reason to not specify it, and you would only reap negative consequences for not doing so. +/// +/// Valid reasons to specify `None` as the `target` are, for example, if you parse a type from a +/// file and do not know the context in which it will be used. Be careful when doing this; ideally, +/// find a way to find out the context in which the type will be used. +/// +/// ## Safety +/// +/// [Constrained] does not guarantee that a validated type will always be *correct* in the context +/// it is in. /// /// ### Example /// /// The password "123" might be well-formed, as in, it meets the validation criteria specified by /// the system. However, this makes no implications about "123" being the correct password for a /// given user account. -pub(crate) trait Constrained { - fn validate(&self) -> Result<(), ConstraintError>; +pub trait Constrained { + /// Perform validation on the type, returning an error if the type is not well-formed. + fn validate(&self, target: Option) -> Result<(), ConstraintError>; +} + +#[cfg(test)] +pub(crate) mod testing_utils { + pub(crate) fn init_logger() { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "trace"); + } + env_logger::builder() + .filter_module("crate", log::LevelFilter::Trace) + .try_init() + .unwrap_or(()); + } } #[cfg(test)] mod test { use der::asn1::Uint; - use x509_cert::certificate::Profile; - use x509_cert::serial_number::SerialNumber; - - #[derive(Clone, PartialEq, Eq, Debug)] - enum TestProfile {} - impl Profile for TestProfile {} + use crate::types::x509_cert::SerialNumber; fn strip_leading_zeroes(bytes: &[u8]) -> &[u8] { if let Some(stripped) = bytes.strip_prefix(&[0u8]) { @@ -113,8 +192,7 @@ mod test { fn test_convert_serial_number() { let biguint = Uint::new(&[10u8, 240u8]).unwrap(); assert_eq!(biguint.as_bytes(), &[10u8, 240u8]); - let serial_number: SerialNumber = - SerialNumber::new(biguint.as_bytes()).unwrap(); + let serial_number = SerialNumber::new(biguint.as_bytes()).unwrap(); assert_eq!( strip_leading_zeroes(serial_number.as_bytes()), biguint.as_bytes() @@ -122,8 +200,7 @@ mod test { let biguint = Uint::new(&[240u8, 10u8]).unwrap(); assert_eq!(biguint.as_bytes(), &[240u8, 10u8]); - let serial_number: SerialNumber = - SerialNumber::new(biguint.as_bytes()).unwrap(); + let serial_number = SerialNumber::new(biguint.as_bytes()).unwrap(); assert_eq!( strip_leading_zeroes(serial_number.as_bytes()), biguint.as_bytes() diff --git a/src/signature.rs b/src/signature.rs index 6f0675e..8d5c78f 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -5,12 +5,13 @@ use spki::{AlgorithmIdentifierOwned, SignatureBitStringEncoding}; /// A signature value, generated using a [SignatureAlgorithm] -pub trait Signature: PartialEq + Eq + SignatureBitStringEncoding + Clone { +pub trait Signature: PartialEq + Eq + SignatureBitStringEncoding + Clone + ToString { + /// The underlying signature type type Signature; /// The signature value fn as_signature(&self) -> &Self::Signature; /// The [AlgorithmIdentifierOwned] associated with this signature fn algorithm_identifier() -> AlgorithmIdentifierOwned; - /// From a bit string signature value, create a new [Self] - fn from_bitstring(signature: &[u8]) -> Self; + /// From a byte slice, create a new [Self] + fn from_bytes(signature: &[u8]) -> Self; } diff --git a/src/types/challenge_string.rs b/src/types/challenge_string.rs new file mode 100644 index 0000000..36a76fd --- /dev/null +++ b/src/types/challenge_string.rs @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// A struct that holds a challenge string and its expiration time. +pub struct ChallengeString { + /// The challenge, as generated by the polyproto home server. + pub challenge: String, + /// An expiry date in seconds since the Unix epoch, after which the challenge cannot be completed + /// any longer. + pub expires: u64, +} diff --git a/src/types/der/asn1/ia5string.rs b/src/types/der/asn1/ia5string.rs new file mode 100644 index 0000000..442cb71 --- /dev/null +++ b/src/types/der/asn1/ia5string.rs @@ -0,0 +1,126 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)] +/// Wrapper around [der::asn1::Ia5String], which provides serde support, if the `serde` feature is +/// enabled. +/// +/// ASN.1 `IA5String` type. +/// +/// Supports the [International Alphabet No. 5 (IA5)] character encoding, i.e. +/// the lower 128 characters of the ASCII alphabet. (Note: IA5 is now +/// technically known as the International Reference Alphabet or IRA as +/// specified in the ITU-T's T.50 recommendation). +/// +/// For UTF-8, use [`String`][`alloc::string::String`]. +/// +/// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_%28standard%29 +pub struct Ia5String(der::asn1::Ia5String); + +impl Ia5String { + /// Create a new `IA5String`. + pub fn new(input: &T) -> Result + where + T: AsRef<[u8]> + ?Sized, + { + Ok(Ia5String(der::asn1::Ia5String::new(input)?)) + } +} + +impl Deref for Ia5String { + type Target = der::asn1::Ia5String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Ia5String { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Ia5String { + fn from(s: der::asn1::Ia5String) -> Self { + Self(s) + } +} + +impl From for der::asn1::Ia5String { + fn from(s: Ia5String) -> Self { + s.0 + } +} + +#[cfg(feature = "serde")] +mod serde_support { + use super::Ia5String; + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + impl<'de> Deserialize<'de> for Ia5String { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(Ia5StringVisitor) + } + } + + struct Ia5StringVisitor; + + impl<'de> Visitor<'de> for Ia5StringVisitor { + type Value = Ia5String; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str( + "a concatenation of characters from the IA5 character set in &str format", + ) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(Ia5String(match der::asn1::Ia5String::new(&v.to_string()) { + Ok(val) => val, + Err(e) => return Err(E::custom(e)), + })) + } + } + + impl Serialize for Ia5String { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.0.to_string().as_str()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_test::{assert_de_tokens, assert_tokens, Token}; + + #[test] + fn ia5string_ser() { + let ia5string = Ia5String(der::asn1::Ia5String::new("test").unwrap()); + assert_tokens(&ia5string, &[Token::Str("test")]); + let ia5string = Ia5String(der::asn1::Ia5String::new(&64u64.to_string()).unwrap()); + assert_tokens(&ia5string, &[Token::Str("64")]); + } + + #[test] + fn ia5string_de() { + let ia5string = Ia5String(der::asn1::Ia5String::new("test").unwrap()); + assert_de_tokens(&ia5string, &[Token::Str("test")]); + let ia5string = Ia5String(der::asn1::Ia5String::new(64u64.to_string().as_str()).unwrap()); + assert_de_tokens(&ia5string, &[Token::Str("64")]); + } +} diff --git a/src/types/der/asn1/mod.rs b/src/types/der/asn1/mod.rs new file mode 100644 index 0000000..e0664cc --- /dev/null +++ b/src/types/der/asn1/mod.rs @@ -0,0 +1,10 @@ +// Copyright (c) 2024 bitfl0wer +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#[allow(missing_docs)] +pub mod ia5string; + +pub use ia5string::*; diff --git a/src/types/der/mod.rs b/src/types/der/mod.rs new file mode 100644 index 0000000..ff3ec15 --- /dev/null +++ b/src/types/der/mod.rs @@ -0,0 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#[allow(missing_docs)] +pub mod asn1; diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs new file mode 100644 index 0000000..8cbe2b6 --- /dev/null +++ b/src/types/encrypted_pkm.rs @@ -0,0 +1,104 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use der::asn1::BitString; + +use super::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfo}; +use super::x509_cert::SerialNumber; + +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +/// A private key material structure for storing encrypted private key material on a home server. +/// +/// For more information, such as how this type is represented in JSON, see the type definition of +/// `EncryptedPKM` on the [polyproto documentation website](https://docs.polyphony.chat/APIs/core/Types/encrypted_pkm/) +pub struct EncryptedPkm { + /// The serial number of the certificate that this private key material is associated with. + pub serial_number: SerialNumber, + /// The encrypted private key material, along with the signature algorithm of the private key. + pub key_data: PrivateKeyInfo, + /// The encryption algorithm used to encrypt the private key material. + pub encryption_algorithm: AlgorithmIdentifierOwned, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// Private key material with additional information about the private keys' algorithm. +pub struct PrivateKeyInfo { + /// The algorithm of the private key. + pub algorithm: AlgorithmIdentifierOwned, + /// The encrypted private key material. + pub encrypted_private_key_bitstring: BitString, +} + +impl From for PrivateKeyInfo { + fn from(value: SubjectPublicKeyInfo) -> Self { + PrivateKeyInfo { + algorithm: value.algorithm.clone().into(), + encrypted_private_key_bitstring: value.subject_public_key.clone(), + } + } +} + +impl From for SubjectPublicKeyInfo { + fn from(value: PrivateKeyInfo) -> Self { + spki::SubjectPublicKeyInfoOwned { + algorithm: value.algorithm.into(), + subject_public_key: value.encrypted_private_key_bitstring, + } + .into() + } +} + +#[cfg(feature = "serde")] +mod serde_support { + use der::pem::LineEnding; + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + use crate::types::spki::SubjectPublicKeyInfo; + + use super::PrivateKeyInfo; + + struct PrivateKeyInfoVisitor; + + impl<'de> Visitor<'de> for PrivateKeyInfoVisitor { + type Value = PrivateKeyInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a private key info structure, which is a subject public key info structure as defined in RFC 5280. this private key info structure needs to be a valid PEM encoded ASN.1 structure") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + SubjectPublicKeyInfo::from_pem(v.as_bytes()) + .map_err(serde::de::Error::custom) + .map(Into::into) + } + } + + impl<'de> Deserialize<'de> for crate::types::encrypted_pkm::PrivateKeyInfo { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(PrivateKeyInfoVisitor) + } + } + + impl Serialize for crate::types::encrypted_pkm::PrivateKeyInfo { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str( + SubjectPublicKeyInfo::from(self.clone()) + .to_pem(LineEnding::LF) + .map_err(serde::ser::Error::custom)? + .as_str(), + ) + } + } +} diff --git a/src/types/federation_id.rs b/src/types/federation_id.rs new file mode 100644 index 0000000..d4c4559 --- /dev/null +++ b/src/types/federation_id.rs @@ -0,0 +1,65 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::ops::{Deref, DerefMut}; + +use regex::Regex; + +use crate::errors::{ConstraintError, ERR_MSG_FEDERATION_ID_REGEX}; +use crate::Constrained; + +/// The regular expression for a valid `FederationId`. +pub static REGEX_FEDERATION_ID: &str = r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)"; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// A `FederationId` is a globally unique identifier for an actor in the context of polyproto. +pub struct FederationId { + pub(crate) inner: String, +} + +impl Deref for FederationId { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for FederationId { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl FederationId { + /// Validates input, then creates a new `FederationId`. + pub fn new(id: &str) -> Result { + let regex = Regex::new(REGEX_FEDERATION_ID).unwrap(); + let matches = { + let mut x = String::new(); + regex + .find_iter(id) + .map(|y| y.as_str()) + .for_each(|y| x.push_str(y)); + x + }; + if regex.is_match(&matches) { + let fid = Self { + inner: matches.to_string(), + }; + fid.validate(None)?; + Ok(fid) + } else { + Err(ConstraintError::Malformed(Some( + ERR_MSG_FEDERATION_ID_REGEX.to_string(), + ))) + } + } +} + +impl std::fmt::Display for FederationId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..047fdb1 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,110 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +/// Module defining the [ChallengeString] type. +pub mod challenge_string; +/// This module contains wrappers for types from the `der` crate which interface directly with the +/// HTTP API of polyproto. These wrappers enable the types to be serialized and deserialized using +/// the `serde` crate, if the `serde` feature is enabled. +pub mod der; +/// Module defining the [EncryptedPkm] type, as well as related subtypes. +pub mod encrypted_pkm; +/// Module defining the [FederationId] type. +pub mod federation_id; +/// This module contains wrappers for types from the `spki` crate which interface directly with the +/// HTTP API of polyproto. These wrappers enable the types to be serialized and deserialized using +/// the `serde` crate, if the `serde` feature is enabled. +pub mod spki; +/// This module contains wrappers for types from the `x509_cert` crate which interface directly with the +/// HTTP API of polyproto. These wrappers enable the types to be serialized and deserialized using +/// the `serde` crate, if the `serde` feature is enabled. +pub mod x509_cert; + +pub use challenge_string::*; +pub use encrypted_pkm::*; +pub use federation_id::*; + +/// Module defining the [Route] type, as well as `static` endpoints and their associated HTTP methods +/// for the polyproto API. These `static`s can be used as a single source of truth for the API endpoints +/// and what methods to submit to them. +pub mod routes { + #[derive(Debug, Clone)] + /// A route, consisting of an HTTP method and a path, which is relative to the root of the polyproto + /// server URL. + #[allow(missing_docs)] + pub struct Route { + pub method: http::Method, + pub path: &'static str, + } + + #[cfg(not(tarpaulin_include))] + /// [Route]s for the core API of polyproto. + pub mod core { + /// [Route]s for version 1 of polyproto. + pub mod v1 { + #![allow(missing_docs)] + use super::super::Route; + + pub static GET_CHALLENGE_STRING: Route = Route { + method: http::Method::GET, + path: "/.p2/core/v1/challenge", + }; + + pub static ROTATE_SERVER_IDENTITY_KEY: Route = Route { + method: http::Method::PUT, + path: "/.p2/core/v1/key/server", + }; + + pub static GET_SERVER_PUBLIC_IDCERT: Route = Route { + method: http::Method::GET, + path: "/.p2/core/v1/idcert/server", + }; + + pub static GET_SERVER_PUBLIC_KEY: Route = Route { + method: http::Method::GET, + path: "/.p2/core/v1/key/server", + }; + + pub static GET_ACTOR_IDCERTS: Route = Route { + method: http::Method::GET, + path: "/.p2/core/v1/idcert/actor/", + }; + + pub static UPDATE_SESSION_IDCERT: Route = Route { + method: http::Method::PUT, + path: "/.p2/core/v1/session/idcert/extern", + }; + + pub static DELETE_SESSION: Route = Route { + method: http::Method::DELETE, + path: "/.p2/core/v1/session/", + }; + + pub static ROTATE_SESSION_IDCERT: Route = Route { + method: http::Method::POST, + path: "/.p2/core/v1/session/idcert", + }; + + pub static UPLOAD_ENCRYPTED_PKM: Route = Route { + method: http::Method::POST, + path: "/.p2/core/v1/session/keymaterial", + }; + + pub static GET_ENCRYPTED_PKM: Route = Route { + method: http::Method::GET, + path: "/.p2/core/v1/session/keymaterial", + }; + + pub static DELETE_ENCRYPTED_PKM: Route = Route { + method: http::Method::DELETE, + path: "/.p2/core/v1/session/keymaterial", + }; + + pub static GET_ENCRYPTED_PKM_UPLOAD_SIZE_LIMIT: Route = Route { + method: http::Method::OPTIONS, + path: "/.p2/core/v1/session/keymaterial", + }; + } + } +} diff --git a/src/types/spki/algorithmidentifierowned.rs b/src/types/spki/algorithmidentifierowned.rs new file mode 100644 index 0000000..b49e3cc --- /dev/null +++ b/src/types/spki/algorithmidentifierowned.rs @@ -0,0 +1,158 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::ops::{Deref, DerefMut}; + +use der::{Any, Decode, Encode}; +use spki::ObjectIdentifier; + +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] +/// `AlgorithmIdentifier` reference which has `Any` parameters. +/// +/// A wrapper around `spki::AlgorithmIdentifierOwned`, which provides `serde` support, if enabled by +/// the `serde` feature. +/// +/// ## De-/Serialization expectations +/// +/// This type expects a DER encoded AlgorithmIdentifier with optional der::Any parameters. The DER +/// encoded data has to be provided in the form of an array of bytes. Types that fulfill this +/// expectation are, for example, `&[u8]`, `Vec` and `&[u8; N]`. +pub struct AlgorithmIdentifierOwned(spki::AlgorithmIdentifierOwned); + +impl AlgorithmIdentifierOwned { + /// Create a new `AlgorithmIdentifierOwned`. + pub fn new(oid: ObjectIdentifier, parameters: Option) -> Self { + Self(spki::AlgorithmIdentifierOwned { oid, parameters }) + } + + /// Try to encode this type as DER. + pub fn to_der(&self) -> Result, der::Error> { + self.0.to_der() + } + + /// Try to decode this type from DER. + pub fn from_der(bytes: &[u8]) -> Result { + spki::AlgorithmIdentifierOwned::from_der(bytes).map(Self) + } +} + +impl Deref for AlgorithmIdentifierOwned { + type Target = spki::AlgorithmIdentifierOwned; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AlgorithmIdentifierOwned { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for AlgorithmIdentifierOwned { + fn from(value: spki::AlgorithmIdentifierOwned) -> Self { + Self(value) + } +} + +impl From for spki::AlgorithmIdentifierOwned { + fn from(value: AlgorithmIdentifierOwned) -> Self { + value.0 + } +} + +#[cfg(feature = "serde")] +mod serde_support { + use super::AlgorithmIdentifierOwned; + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + struct AlgorithmIdentifierVisitor; + + impl<'de> Visitor<'de> for AlgorithmIdentifierVisitor { + type Value = AlgorithmIdentifierOwned; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter + .write_str("a valid DER encoded byte slice representing an AlgorithmIdentifier") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + AlgorithmIdentifierOwned::from_der(v).map_err(serde::de::Error::custom) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut bytes: Vec = Vec::new(); // Create a new Vec to store the bytes + while let Some(byte) = seq.next_element()? { + // "Iterate" over the sequence, assuming each element is a byte + bytes.push(byte) // Push the byte to the Vec + } + AlgorithmIdentifierOwned::from_der(&bytes).map_err(serde::de::Error::custom) + } + } + + impl<'de> Deserialize<'de> for AlgorithmIdentifierOwned { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_bytes(AlgorithmIdentifierVisitor) + } + } + + impl Serialize for AlgorithmIdentifierOwned { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let der = self.to_der().map_err(serde::ser::Error::custom)?; + serializer.serialize_bytes(&der) + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use der::asn1::BitString; + use der::{Any, Decode, Encode}; + use log::trace; + use serde_json::json; + use spki::ObjectIdentifier; + + use crate::testing_utils::init_logger; + + use super::AlgorithmIdentifierOwned; + + #[test] + fn de_serialize() { + init_logger(); + let oid = ObjectIdentifier::from_str("1.1.1.4.5").unwrap(); + let alg = AlgorithmIdentifierOwned::new(oid, None); + let json = json!(alg); + let deserialized: AlgorithmIdentifierOwned = serde_json::from_value(json).unwrap(); + assert_eq!(alg, deserialized); + trace!("deserialized: {:?}", deserialized); + trace!("original: {:?}", alg); + + let bytes = [48, 6, 6, 3, 43, 6, 1, 5, 1, 4, 5, 5, 23, 2, 0, 0]; + let bitstring = BitString::from_bytes(&bytes).unwrap(); + let alg = AlgorithmIdentifierOwned::new( + oid, + Some(Any::from_der(&bitstring.to_der().unwrap()).unwrap()), + ); + let json = json!(alg); + let deserialized: AlgorithmIdentifierOwned = serde_json::from_value(json).unwrap(); + trace!("deserialized: {:?}", deserialized); + trace!("original: {:?}", alg); + assert_eq!(alg, deserialized); + } +} diff --git a/src/types/spki/mod.rs b/src/types/spki/mod.rs new file mode 100644 index 0000000..b450da1 --- /dev/null +++ b/src/types/spki/mod.rs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![allow(missing_docs)] + +pub mod algorithmidentifierowned; +pub mod subjectpublickeyinfo; + +pub use algorithmidentifierowned::*; +pub use subjectpublickeyinfo::*; diff --git a/src/types/spki/subjectpublickeyinfo.rs b/src/types/spki/subjectpublickeyinfo.rs new file mode 100644 index 0000000..23e93ce --- /dev/null +++ b/src/types/spki/subjectpublickeyinfo.rs @@ -0,0 +1,196 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::ops::{Deref, DerefMut}; + +use super::super::spki::AlgorithmIdentifierOwned; +use der::asn1::BitString; +use der::pem::LineEnding; +use der::{Decode, DecodePem, Encode, EncodePem}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjectPublicKeyInfo(spki::SubjectPublicKeyInfoOwned); + +impl SubjectPublicKeyInfo { + pub fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { + Self(spki::SubjectPublicKeyInfoOwned { + algorithm: algorithm.into(), + subject_public_key, + }) + } + + /// Try to decode this type from PEM. + pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { + spki::SubjectPublicKeyInfo::from_pem(pem).map(Self) + } + /// Try to decode this type from DER. + pub fn from_der(value: &[u8]) -> Result { + spki::SubjectPublicKeyInfo::from_der(value).map(Self) + } + + /// Try to encode this type as PEM. + pub fn to_pem(&self, line_ending: LineEnding) -> Result { + self.0.to_pem(line_ending) + } + + /// Try to encode this type as DER. + pub fn to_der(&self) -> Result, der::Error> { + self.0.to_der() + } +} + +impl From for SubjectPublicKeyInfo { + fn from(spki: spki::SubjectPublicKeyInfoOwned) -> Self { + Self(spki) + } +} + +impl From for spki::SubjectPublicKeyInfoOwned { + fn from(spki: SubjectPublicKeyInfo) -> Self { + spki.0 + } +} + +impl Deref for SubjectPublicKeyInfo { + type Target = spki::SubjectPublicKeyInfoOwned; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SubjectPublicKeyInfo { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(feature = "serde")] +mod serde_support { + use der::pem::LineEnding; + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + use super::SubjectPublicKeyInfo; + struct SubjectPublicKeyInfoVisitor; + + impl<'de> Visitor<'de> for SubjectPublicKeyInfoVisitor { + type Value = SubjectPublicKeyInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid, PEM or DER encoded SubjectPublicKeyInfo") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + SubjectPublicKeyInfo::from_pem(v).map_err(serde::de::Error::custom) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + SubjectPublicKeyInfo::from_der(v).map_err(serde::de::Error::custom) + } + } + + impl<'de> Deserialize<'de> for SubjectPublicKeyInfo { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(SubjectPublicKeyInfoVisitor) + } + } + + impl Serialize for SubjectPublicKeyInfo { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let pem = self + .to_pem(LineEnding::default()) + .map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&pem) + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use der::asn1::BitString; + use serde_json::json; + use spki::ObjectIdentifier; + + use crate::types::spki::AlgorithmIdentifierOwned; + + use super::SubjectPublicKeyInfo; + + #[test] + fn deserialize_serialize_spki_json() { + let oids = [ + ObjectIdentifier::from_str("1.1.3.1").unwrap(), + ObjectIdentifier::from_str("2.23.5672.1").unwrap(), + ObjectIdentifier::from_str("0.3.1.1").unwrap(), + ObjectIdentifier::from_str("1.2.3.4.5.6.7.8.9.0.12.3.4.5.6.67").unwrap(), + ObjectIdentifier::from_str("1.2.1122").unwrap(), + ]; + + for oid in oids.into_iter() { + let spki = SubjectPublicKeyInfo::new( + AlgorithmIdentifierOwned::new(oid, None), + BitString::from_bytes(&[0x00, 0x01, 0x02]).unwrap(), + ); + let spki_json = json!(&spki); + let spki2: SubjectPublicKeyInfo = serde_json::from_value(spki_json.clone()).unwrap(); + assert_eq!(spki, spki2); + } + } + + #[test] + fn deserialize_serialize_spki_pem() { + let oids = [ + ObjectIdentifier::from_str("1.1.3.1").unwrap(), + ObjectIdentifier::from_str("2.23.5672.1").unwrap(), + ObjectIdentifier::from_str("0.3.1.1").unwrap(), + ObjectIdentifier::from_str("1.2.3.4.5.6.7.8.9.0.12.3.4.5.6.67").unwrap(), + ObjectIdentifier::from_str("1.2.1122").unwrap(), + ]; + + for oid in oids.into_iter() { + let spki = SubjectPublicKeyInfo::new( + AlgorithmIdentifierOwned::new(oid, None), + BitString::from_bytes(&[0x00, 0x01, 0x02]).unwrap(), + ); + let spki_pem = spki.to_pem(der::pem::LineEnding::LF).unwrap(); + let spki2 = SubjectPublicKeyInfo::from_pem(spki_pem).unwrap(); + assert_eq!(spki, spki2); + } + } + + #[test] + fn deserialize_serialize_spki_der() { + let oids = [ + ObjectIdentifier::from_str("1.1.3.1").unwrap(), + ObjectIdentifier::from_str("2.23.5672.1").unwrap(), + ObjectIdentifier::from_str("0.3.1.1").unwrap(), + ObjectIdentifier::from_str("1.2.3.4.5.6.7.8.9.0.12.3.4.5.6.67").unwrap(), + ObjectIdentifier::from_str("1.2.1122").unwrap(), + ]; + + for oid in oids.into_iter() { + let spki = SubjectPublicKeyInfo::new( + AlgorithmIdentifierOwned::new(oid, None), + BitString::from_bytes(&[0x00, 0x01, 0x02]).unwrap(), + ); + let spki_der = spki.to_der().unwrap(); + let spki2 = SubjectPublicKeyInfo::from_der(&spki_der).unwrap(); + assert_eq!(spki, spki2); + } + } +} diff --git a/src/types/x509_cert/mod.rs b/src/types/x509_cert/mod.rs new file mode 100644 index 0000000..e9c82eb --- /dev/null +++ b/src/types/x509_cert/mod.rs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![allow(missing_docs)] + +pub mod serialnumber; + +pub use serialnumber::*; diff --git a/src/types/x509_cert/serialnumber.rs b/src/types/x509_cert/serialnumber.rs new file mode 100644 index 0000000..377b9ed --- /dev/null +++ b/src/types/x509_cert/serialnumber.rs @@ -0,0 +1,245 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::ops::{Deref, DerefMut}; + +use log::trace; + +use crate::errors::{ConversionError, InvalidInput}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Wrapper type around [x509_cert::serial_number::SerialNumber], providing serde support, if the +/// `serde` feature is enabled. See "De-/serialization value expectations" below for more +/// information. +/// +/// [RFC 5280 Section 4.1.2.2.] Serial Number +/// +/// The serial number MUST be a positive integer assigned by the CA to +/// each certificate. It MUST be unique for each certificate issued by a +/// given CA (i.e., the issuer name and serial number identify a unique +/// certificate). CAs MUST force the serialNumber to be a non-negative +/// integer. +/// +/// Given the uniqueness requirements above, serial numbers can be +/// expected to contain long integers. Certificate users MUST be able to +/// handle serialNumber values up to 20 octets. Conforming CAs MUST NOT +/// use serialNumber values longer than 20 octets. +/// +/// Note: Non-conforming CAs may issue certificates with serial numbers +/// that are negative or zero. Certificate users SHOULD be prepared to +/// gracefully handle such certificates. +/// +/// ## De-/serialization value expectations +/// +/// The serde de-/serialization implementation for [`SerialNumber`] expects a byte slice representing +/// a positive integer. +pub struct SerialNumber(::x509_cert::serial_number::SerialNumber); + +impl From<::x509_cert::serial_number::SerialNumber> for SerialNumber { + fn from(inner: ::x509_cert::serial_number::SerialNumber) -> Self { + SerialNumber(inner) + } +} + +impl From for ::x509_cert::serial_number::SerialNumber { + fn from(value: SerialNumber) -> Self { + value.0 + } +} + +impl Deref for SerialNumber { + type Target = ::x509_cert::serial_number::SerialNumber; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SerialNumber { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl SerialNumber { + /// Create a new [`SerialNumber`] from a byte slice. + /// + /// The byte slice **must** represent a positive integer. + pub fn new(bytes: &[u8]) -> Result { + x509_cert::serial_number::SerialNumber::new(bytes).map(Into::into) + } + + /// Borrow the inner byte slice which contains the least significant bytes + /// of a big endian integer value with all leading zeros stripped. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Try to convert the inner byte slice to a [u128]. + /// + /// Returns an error if the byte slice is empty, + /// or if the byte slice is longer than 16 bytes. Leading zeros of byte slices are stripped, so + /// 17 bytes are allowed, if the first byte is zero. + pub fn try_as_u128(&self) -> Result { + let mut bytes = self.as_bytes().to_vec(); + if bytes.is_empty() { + return Err(InvalidInput::Length { + min_length: 1, + max_length: 16, + actual_length: 1.to_string(), + } + .into()); + } + if *bytes.first().unwrap() == 0 { + bytes.remove(0); + } + trace!("bytes: {:?}", bytes); + if bytes.len() > 16 { + return Err(InvalidInput::Length { + min_length: 1, + max_length: 16, + actual_length: bytes.len().to_string(), + } + .into()); + } + let mut buf = [0u8; 16]; + buf[16 - bytes.len()..].copy_from_slice(&bytes); + Ok(u128::from_be_bytes(buf)) + } +} + +impl TryFrom for u128 { + type Error = ConversionError; + + fn try_from(value: SerialNumber) -> Result { + value.try_as_u128() + } +} + +impl From for SerialNumber { + fn from(value: u128) -> Self { + // All u128 values are valid serial numbers, so we can unwrap + SerialNumber::new(&value.to_be_bytes()).unwrap() + } +} + +#[cfg(feature = "serde")] +mod serde_support { + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + use super::SerialNumber; + + struct SerialNumberVisitor; + + impl<'de> Visitor<'de> for SerialNumberVisitor { + type Value = SerialNumber; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a byte slice representing a positive integer") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + SerialNumber::new(v).map_err(serde::de::Error::custom) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut bytes: Vec = Vec::new(); // Create a new Vec to store the bytes + while let Some(byte) = seq.next_element()? { + // "Iterate" over the sequence, assuming each element is a byte + bytes.push(byte) // Push the byte to the Vec + } + SerialNumber::new(&bytes).map_err(serde::de::Error::custom) // Create a SerialNumber from the Vec + } + } + + impl<'de> Deserialize<'de> for SerialNumber { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(SerialNumberVisitor) + } + } + + impl Serialize for SerialNumber { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.as_bytes()) + } + } +} + +#[cfg(test)] +mod test { + use log::trace; + use serde_json::json; + + use crate::testing_utils::init_logger; + + use super::SerialNumber; + + #[test] + fn serialize_deserialize() { + init_logger(); + let serial_number = SerialNumber::new(&2347812387874u128.to_be_bytes()).unwrap(); + let serialized = json!(serial_number); + trace!("is_array: {:?}", serialized.is_array()); + trace!("serialized: {}", serialized); + let deserialized: SerialNumber = serde_json::from_value(serialized).unwrap(); + + assert_eq!(serial_number, deserialized); + } + + #[test] + fn serial_number_from_to_u128() { + init_logger(); + let mut val = 0u128; + loop { + let serial_number = SerialNumber::new(&val.to_be_bytes()).unwrap(); + let json = json!(serial_number); + let deserialized: SerialNumber = serde_json::from_value(json).unwrap(); + let u128 = deserialized.try_as_u128().unwrap(); + assert_eq!(u128, val); + assert_eq!(deserialized, serial_number); + if val == 0 { + val = 1; + } + if val == u128::MAX { + break; + } + val = match val.checked_mul(2) { + Some(v) => v, + None => u128::MAX, + }; + } + } + + #[test] + fn try_as_u128() { + init_logger(); + let mut val = 1u128; + loop { + let serial_number = SerialNumber::new(&val.to_be_bytes()).unwrap(); + let u128 = serial_number.try_as_u128().unwrap(); + assert_eq!(u128, val); + trace!("u128: {}", u128); + if val == u128::MAX { + break; + } + val = match val.checked_mul(2) { + Some(v) => v, + None => u128::MAX, + }; + } + } +} diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs new file mode 100644 index 0000000..f4b9c53 --- /dev/null +++ b/tests/api/core/mod.rs @@ -0,0 +1,508 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use der::asn1::{BitString, GeneralizedTime, Uint}; +use httptest::matchers::request::method_path; +use httptest::matchers::{eq, json_decoded, matches, request}; +use httptest::responders::{json_encoded, status_code}; +use httptest::*; +use polyproto::api::core::current_unix_time; +use polyproto::certs::capabilities::Capabilities; +use polyproto::certs::idcert::IdCert; +use polyproto::certs::idcsr::IdCsr; +use polyproto::certs::SessionId; +use polyproto::key::PublicKey; +use polyproto::types::routes::core::v1::{ + DELETE_ENCRYPTED_PKM, DELETE_SESSION, GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, + GET_ENCRYPTED_PKM, GET_ENCRYPTED_PKM_UPLOAD_SIZE_LIMIT, GET_SERVER_PUBLIC_IDCERT, + GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, ROTATE_SESSION_IDCERT, + UPDATE_SESSION_IDCERT, UPLOAD_ENCRYPTED_PKM, +}; +use polyproto::types::spki::AlgorithmIdentifierOwned; +use polyproto::types::x509_cert::SerialNumber; +use polyproto::types::{EncryptedPkm, PrivateKeyInfo}; +use serde_json::json; +use spki::ObjectIdentifier; +use x509_cert::time::Validity; + +use crate::common::{ + actor_id_cert, actor_subject, default_validity, gen_priv_key, home_server_id_cert, + home_server_subject, init_logger, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature, +}; + +/// Correctly format the server URL for the test. +fn server_url(server: &Server) -> String { + format!("http://{}", server.addr()) +} + +#[tokio::test] +async fn get_challenge_string() { + init_logger(); + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path( + GET_CHALLENGE_STRING.method.as_str(), + GET_CHALLENGE_STRING.path, + )) + .respond_with(json_encoded(json!({ + "challenge": "a".repeat(32), + "expires": 1 + }))), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let challenge_string = client.get_challenge_string().await.unwrap(); + assert_eq!(challenge_string.challenge, "a".repeat(32)); + assert_eq!(challenge_string.expires, 1); +} + +#[tokio::test] +async fn rotate_server_identity_key() { + init_logger(); + let home_server_signing_key = gen_priv_key(); + let id_csr = IdCsr::new( + &home_server_subject(), + &home_server_signing_key, + &Capabilities::default_home_server(), + Some(polyproto::certs::Target::HomeServer), + ) + .unwrap(); + let id_cert = IdCert::from_ca_csr( + id_csr, + &home_server_signing_key, + Uint::new(9u64.to_be_bytes().as_slice()).unwrap(), + home_server_subject(), + Validity { + not_before: x509_cert::time::Time::GeneralTime( + GeneralizedTime::from_unix_duration(std::time::Duration::new( + current_unix_time() - 1000, + 0, + )) + .unwrap(), + ), + not_after: x509_cert::time::Time::GeneralTime( + GeneralizedTime::from_unix_duration(std::time::Duration::new( + current_unix_time() + 1000, + 0, + )) + .unwrap(), + ), + }, + ) + .unwrap(); + let cert_pem = id_cert.to_pem(der::pem::LineEnding::LF).unwrap(); + let server = Server::run(); + server.expect( + Expectation::matching(method_path( + ROTATE_SERVER_IDENTITY_KEY.method.as_str(), + ROTATE_SERVER_IDENTITY_KEY.path, + )) + .respond_with(json_encoded(json!(cert_pem))), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let cert = client + .rotate_server_identity_key::() + .await + .unwrap(); + assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); +} + +#[tokio::test] +async fn get_server_public_key() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + let public_key_info = priv_key.public_key.public_key_info(); + let pem = public_key_info.to_pem(der::pem::LineEnding::LF).unwrap(); + log::debug!("Generated Public Key:\n{}", pem); + let server = Server::run(); + server.expect( + Expectation::matching(method_path( + GET_SERVER_PUBLIC_KEY.method.as_str(), + GET_SERVER_PUBLIC_KEY.path, + )) + .respond_with(json_encoded(json!(public_key_info + .to_pem(der::pem::LineEnding::LF) + .unwrap()))), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let public_key = client.get_server_public_key_info(None).await.unwrap(); + log::debug!( + "Received Public Key:\n{}", + public_key.to_pem(der::pem::LineEnding::LF).unwrap() + ); + assert_eq!( + public_key.public_key_bitstring, + priv_key.public_key.public_key_info().public_key_bitstring + ); +} + +#[tokio::test] +async fn get_server_id_cert() { + init_logger(); + let id_cert = home_server_id_cert(); + let cert_pem = id_cert.to_pem(der::pem::LineEnding::LF).unwrap(); + let server = Server::run(); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + server.expect( + Expectation::matching(all_of![ + request::method(GET_SERVER_PUBLIC_IDCERT.method.as_str()), + request::path(GET_SERVER_PUBLIC_IDCERT.path), + request::body(json_decoded(eq(json!({"timestamp": 10})))), + ]) + .respond_with(json_encoded(json!(cert_pem))), + ); + + let cert = client + .get_server_id_cert::(Some(10)) + .await + .unwrap(); + assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); +} + +#[tokio::test] +async fn get_actor_id_certs() { + init_logger(); + let id_certs = { + let mut vec: Vec> = Vec::new(); + for _ in 0..5 { + let cert = actor_id_cert("flori"); + vec.push(cert); + } + vec + }; + + let certs_pem: Vec = id_certs + .into_iter() + .map(|cert| cert.to_pem(der::pem::LineEnding::LF).unwrap()) + .collect(); + + let server = Server::run(); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + + server.expect( + Expectation::matching(all_of![ + request::method(GET_ACTOR_IDCERTS.method.as_str()), + request::path(matches(format!("^{}.*$", GET_ACTOR_IDCERTS.path))), + request::body(json_decoded(eq(json!({ + "timestamp": 12345, + "session_id": "cool_session_id" + })))) + ]) + .respond_with(json_encoded(json!([{ + "id_cert": certs_pem[0], + "invalidated": false + }]))), + ); + + let certs = client + .get_actor_id_certs::( + "flori@polyphony.chat", + Some(12345), + Some(&SessionId::new_validated("cool_session_id").unwrap()), + ) + .await + .unwrap(); + assert_eq!(certs.len(), 1); + assert_eq!( + certs[0] + .id_cert + .clone() + .to_pem(der::pem::LineEnding::LF) + .unwrap(), + certs_pem[0] + ); + assert!(!certs[0].invalidated); + + server.expect( + Expectation::matching(all_of![ + request::method(GET_ACTOR_IDCERTS.method.as_str()), + request::path(matches(format!("^{}.*$", GET_ACTOR_IDCERTS.path))), + ]) + .respond_with(json_encoded(json!([{ + "id_cert": certs_pem[0], + "invalidated": false + }]))), + ); + let certs = client + .get_actor_id_certs::( + "flori@polyphony.chat", + Some(12345), + Some(&SessionId::new_validated("cool_session_id").unwrap()), + ) + .await + .unwrap(); + assert_eq!(certs.len(), 1); + assert_eq!( + certs[0] + .id_cert + .clone() + .to_pem(der::pem::LineEnding::LF) + .unwrap(), + certs_pem[0] + ); + assert!(!certs[0].invalidated); + + server.expect( + Expectation::matching(all_of![ + request::method(GET_ACTOR_IDCERTS.method.as_str()), + request::path(matches(format!("^{}.*$", GET_ACTOR_IDCERTS.path))), + request::body(json_decoded(eq(json!({ + "timestamp": 12345 })))) + ]) + .respond_with(json_encoded(json!([{ + "id_cert": certs_pem[0], + "invalidated": false + }]))), + ); + + let certs = client + .get_actor_id_certs::( + "flori@polyphony.chat", + Some(12345), + None, + ) + .await + .unwrap(); + assert_eq!(certs.len(), 1); + assert_eq!( + certs[0] + .id_cert + .clone() + .to_pem(der::pem::LineEnding::LF) + .unwrap(), + certs_pem[0] + ); + assert!(!certs[0].invalidated); + + server.expect( + Expectation::matching(all_of![ + request::method(GET_ACTOR_IDCERTS.method.as_str()), + request::path(matches(format!("^{}.*$", GET_ACTOR_IDCERTS.path))), + request::body(json_decoded(eq(json!({ + "session_id": "cool_session_id" + })))) + ]) + .respond_with(json_encoded(json!([{ + "id_cert": certs_pem[0], + "invalidated": false + }]))), + ); + + let certs = client + .get_actor_id_certs::( + "flori@polyphony.chat", + None, + Some(&SessionId::new_validated("cool_session_id").unwrap()), + ) + .await + .unwrap(); + assert_eq!(certs.len(), 1); + assert_eq!( + certs[0] + .id_cert + .clone() + .to_pem(der::pem::LineEnding::LF) + .unwrap(), + certs_pem[0] + ); + assert!(!certs[0].invalidated); +} + +#[tokio::test] +async fn update_session_id_cert() { + init_logger(); + let id_cert = actor_id_cert("flori"); + let cert_pem = id_cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let server = Server::run(); + server.expect( + Expectation::matching(all_of![ + request::method(UPDATE_SESSION_IDCERT.method.to_string()), + request::path(UPDATE_SESSION_IDCERT.path), + request::body(cert_pem) + ]) + .respond_with(status_code(201)), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + client + .update_session_id_cert::(id_cert) + .await + .unwrap(); +} + +#[tokio::test] +async fn delete_session() { + init_logger(); + let server = Server::run(); + server.expect( + Expectation::matching(all_of![ + request::method(DELETE_SESSION.method.to_string()), + request::path(DELETE_SESSION.path), + request::body(json_decoded(eq(json!({ + "session_id": "cool_session_id" + })))) + ]) + .respond_with(status_code(204)), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + client + .delete_session(&SessionId::new_validated("cool_session_id").unwrap()) + .await + .unwrap(); +} + +#[tokio::test] +async fn rotate_session_id_cert() { + init_logger(); + let actor_signing_key = gen_priv_key(); + let home_server_signing_key = gen_priv_key(); + let id_csr = IdCsr::new( + &actor_subject("flori"), + &actor_signing_key, + &Capabilities::default_actor(), + Some(polyproto::certs::Target::Actor), + ) + .unwrap(); + let id_cert = IdCert::from_actor_csr( + id_csr.clone(), + &home_server_signing_key, + Uint::new(&[8]).unwrap(), + home_server_subject(), + default_validity(), + ) + .unwrap(); + let csr_pem = id_csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let server = Server::run(); + server.expect( + Expectation::matching(all_of![ + request::method(ROTATE_SESSION_IDCERT.method.to_string()), + request::path(ROTATE_SESSION_IDCERT.path), + request::body(csr_pem) + ]) + .respond_with(json_encoded(json!({ + "id_cert": id_cert.to_pem(der::pem::LineEnding::LF).unwrap(), + "token": "meow" + }))), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + client + .rotate_session_id_cert::(id_csr) + .await + .unwrap(); +} + +fn encrypted_pkm(serial: u128) -> EncryptedPkm { + let key = gen_priv_key(); + let pkm = String::from_utf8_lossy(key.key.as_bytes()).to_string(); + EncryptedPkm { + serial_number: SerialNumber::new(&serial.to_be_bytes()).unwrap(), + key_data: PrivateKeyInfo { + algorithm: AlgorithmIdentifierOwned::new( + ObjectIdentifier::new("0.1.1.2.3.4.5.3.2.43.23.32").unwrap(), + None, + ), + encrypted_private_key_bitstring: BitString::from_bytes(&{ + let mut pkm = pkm.as_bytes().to_vec(); + pkm.reverse(); + pkm + }) + .unwrap(), + }, + encryption_algorithm: AlgorithmIdentifierOwned::new( + ObjectIdentifier::new("1.34.234.26.53.73").unwrap(), + None, + ), + } +} + +#[tokio::test] +async fn upload_encrypted_pkm() { + init_logger(); + let encrypted_pkm = encrypted_pkm(7923184); + let server = Server::run(); + server.expect( + Expectation::matching(all_of![ + request::method(UPLOAD_ENCRYPTED_PKM.method.to_string()), + request::path(UPLOAD_ENCRYPTED_PKM.path), + request::body(json_decoded(eq(json!([&encrypted_pkm])))) + ]) + .respond_with(status_code(201)), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + client + .upload_encrypted_pkm(vec![encrypted_pkm]) + .await + .unwrap(); +} + +#[tokio::test] +async fn get_encrypted_pkm() { + init_logger(); + let server = Server::run(); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let serial = 7923184u128; + let encrypted_pkm = encrypted_pkm(serial); + server.expect( + Expectation::matching(all_of![ + request::method(GET_ENCRYPTED_PKM.method.to_string()), + request::path(GET_ENCRYPTED_PKM.path), + request::body(json_decoded(eq(json!([serial])))) + ]) + .respond_with(json_encoded(json!([encrypted_pkm]))), + ); + let pkm = client + .get_encrypted_pkm(vec![SerialNumber::from(serial)]) + .await + .unwrap(); + assert_eq!(pkm.first().unwrap(), &encrypted_pkm); +} + +#[tokio::test] +async fn delete_encrypted_pkm() { + init_logger(); + let server = Server::run(); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let serial = 7923184u128; + server.expect( + Expectation::matching(all_of![ + request::method(DELETE_ENCRYPTED_PKM.method.to_string()), + request::path(DELETE_ENCRYPTED_PKM.path), + request::body(json_decoded(eq(json!([serial])))) + ]) + .respond_with(status_code(204)), + ); + + client + .delete_encrypted_pkm(vec![SerialNumber::from(serial)]) + .await + .unwrap(); +} + +#[tokio::test] +async fn get_pkm_upload_size_limit() { + init_logger(); + let server = Server::run(); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let limit = 1024u64; + server.expect( + Expectation::matching(all_of![ + request::method(GET_ENCRYPTED_PKM_UPLOAD_SIZE_LIMIT.method.to_string()), + request::path(GET_ENCRYPTED_PKM_UPLOAD_SIZE_LIMIT.path), + ]) + .respond_with(json_encoded(limit)), + ); + let resp = client.get_pkm_upload_size_limit().await.unwrap(); + assert_eq!(resp, limit); +} diff --git a/tests/api/mod.rs b/tests/api/mod.rs new file mode 100644 index 0000000..7482830 --- /dev/null +++ b/tests/api/mod.rs @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub(crate) mod core; + +use super::*; +use polyproto::types::ChallengeString; +use polyproto::types::FederationId; + +#[test] +fn challenge_string_length() { + let mut thirtytwo = String::from_utf8(vec![121; 32]).unwrap(); + let mut twofivefive = String::from_utf8(vec![121; 255]).unwrap(); + let challenge = ChallengeString { + challenge: thirtytwo.clone(), + expires: 1, + }; + assert!(challenge.validate(None).is_ok()); + let challenge = ChallengeString { + challenge: twofivefive.clone(), + expires: 1, + }; + assert!(challenge.validate(None).is_ok()); + thirtytwo.pop().unwrap(); // String is now 31 characters long + let challenge = ChallengeString { + challenge: thirtytwo, + expires: 1, + }; + assert!(challenge.validate(None).is_err()); + twofivefive.push('a'); // String is now 256 characters long + let challenge = ChallengeString { + challenge: twofivefive, + expires: 1, + }; + assert!(challenge.validate(None).is_err()); +} + +#[test] +fn valid_federation_id() { + FederationId::new("flori@polyphony.chat").unwrap(); + FederationId::new("a@localhost").unwrap(); + FederationId::new("really-long.domain.with-at-least-4-subdomains.or-something@example.com") + .unwrap(); +} + +#[test] +fn invalid_federation_id() { + assert!(FederationId::new("\\@example.com").is_err()); + assert!(FederationId::new("example.com").is_err()); + assert!(FederationId::new("examplecom").is_err()); + assert!(FederationId::new("⾆@example.com").is_err()); + assert!(FederationId::new("example@⾆.com").is_err()); + assert!(FederationId::new("example@😿.com").is_err()); + assert_eq!( + *FederationId::new("example@com.⾆").unwrap(), + "example@com".to_string() + ); +} diff --git a/tests/certs/capabilities/key_usage.rs b/tests/certs/capabilities/key_usage.rs new file mode 100644 index 0000000..0f0e301 --- /dev/null +++ b/tests/certs/capabilities/key_usage.rs @@ -0,0 +1,114 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use der::asn1::BitString; +use log::trace; +use polyproto::certs::capabilities::{KeyUsage, KeyUsages}; + +use crate::common::init_logger; + +#[test] +fn to_bitstring() { + init_logger(); + let key_usages_vec = vec![ + KeyUsage::DigitalSignature, + KeyUsage::ContentCommitment, + KeyUsage::KeyEncipherment, + KeyUsage::DataEncipherment, + KeyUsage::KeyAgreement, + KeyUsage::KeyCertSign, + KeyUsage::CrlSign, + KeyUsage::EncipherOnly, + KeyUsage::DecipherOnly, + ]; + let key_usages = KeyUsages { + key_usages: key_usages_vec, + }; + let bitstring = key_usages.to_bitstring(); + trace!("Unused bits: {}", bitstring.unused_bits()); + assert_eq!(bitstring.raw_bytes(), &[128, 255]); + + let key_usages_vec = vec![KeyUsage::DecipherOnly]; + let key_usages = KeyUsages { + key_usages: key_usages_vec, + }; + let bitstring = key_usages.to_bitstring(); + trace!("Unused bits: {}", bitstring.unused_bits()); + assert_eq!(bitstring.raw_bytes(), &[128, 0]); + + let key_usages_vec = vec![KeyUsage::DigitalSignature]; + let key_usages = KeyUsages { + key_usages: key_usages_vec, + }; + let bitstring = key_usages.to_bitstring(); + trace!("Unused bits: {}", bitstring.unused_bits()); + assert_eq!(bitstring.raw_bytes(), &[128]); + + let key_usages_vec = vec![ + KeyUsage::DigitalSignature, + KeyUsage::ContentCommitment, + KeyUsage::KeyEncipherment, + KeyUsage::DataEncipherment, + KeyUsage::KeyAgreement, + KeyUsage::KeyCertSign, + KeyUsage::CrlSign, + KeyUsage::EncipherOnly, + ]; + let key_usages = KeyUsages { + key_usages: key_usages_vec, + }; + let bitstring = key_usages.to_bitstring(); + trace!("Unused bits: {}", bitstring.unused_bits()); + assert_eq!(bitstring.raw_bytes(), &[255]); +} + +#[test] +fn from_bitstring() { + let bitstring = BitString::new(7, [128, 255]).unwrap(); + let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); + key_usages.key_usages.sort(); + let mut expected = [ + KeyUsage::DigitalSignature, + KeyUsage::ContentCommitment, + KeyUsage::KeyEncipherment, + KeyUsage::DataEncipherment, + KeyUsage::KeyAgreement, + KeyUsage::KeyCertSign, + KeyUsage::CrlSign, + KeyUsage::EncipherOnly, + KeyUsage::DecipherOnly, + ]; + expected.sort(); + assert_eq!(key_usages.key_usages, expected); + + let bitstring = BitString::new(7, [128, 0]).unwrap(); + let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); + key_usages.key_usages.sort(); + let mut expected = [KeyUsage::DecipherOnly]; + expected.sort(); + assert_eq!(key_usages.key_usages, expected); + + let bitstring = BitString::new(0, [128]).unwrap(); + let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); + key_usages.key_usages.sort(); + let mut expected = [KeyUsage::DigitalSignature]; + expected.sort(); + assert_eq!(key_usages.key_usages, expected); + + let bitstring = BitString::new(0, [255]).unwrap(); + let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); + key_usages.key_usages.sort(); + let mut expected = [ + KeyUsage::DigitalSignature, + KeyUsage::ContentCommitment, + KeyUsage::KeyEncipherment, + KeyUsage::DataEncipherment, + KeyUsage::KeyAgreement, + KeyUsage::KeyCertSign, + KeyUsage::CrlSign, + KeyUsage::EncipherOnly, + ]; + expected.sort(); + assert_eq!(key_usages.key_usages, expected); +} diff --git a/tests/certs/capabilities/mod.rs b/tests/certs/capabilities/mod.rs new file mode 100644 index 0000000..5cbb9a0 --- /dev/null +++ b/tests/certs/capabilities/mod.rs @@ -0,0 +1,5 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod key_usage; diff --git a/tests/certs/idcert.rs b/tests/certs/idcert.rs new file mode 100644 index 0000000..30dbcad --- /dev/null +++ b/tests/certs/idcert.rs @@ -0,0 +1,319 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#![allow(unused)] + +use std::str::FromStr; +use std::time::Duration; + +use der::asn1::{BitString, Ia5String, Uint, UtcTime}; +use der::{Decode, Encode}; +use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; +use polyproto::certs::capabilities::{self, Capabilities}; +use polyproto::certs::idcert::IdCert; +use polyproto::certs::{PublicKeyInfo, Target}; +use polyproto::errors::composite::ConversionError; +use polyproto::key::{PrivateKey, PublicKey}; +use polyproto::signature::Signature; +use rand::rngs::OsRng; +use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; +use thiserror::Error; +use x509_cert::attr::Attributes; +use x509_cert::name::RdnSequence; +use x509_cert::request::CertReq; +use x509_cert::time::{Time, Validity}; +use x509_cert::Certificate; + +use crate::common::*; + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn test_create_actor_cert() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + println!("Private Key is: {:?}", priv_key.key.to_bytes()); + println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); + println!(); + let mut capabilities = Capabilities::default_actor(); + capabilities + .key_usage + .key_usages + .push(capabilities::KeyUsage::CrlSign); + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key, + &capabilities, + Some(Target::Actor), + ) + .unwrap(); + let cert = IdCert::from_actor_csr( + csr, + &priv_key, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let cert_data = cert.clone().to_der().unwrap(); + let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); + assert_eq!(cert_data, data); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn test_create_ca_cert() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + println!("Private Key is: {:?}", priv_key.key.to_bytes()); + println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); + println!(); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + &priv_key, + &Capabilities::default_home_server(), + Some(Target::HomeServer), + ) + .unwrap(); + let cert = IdCert::from_ca_csr( + csr, + &priv_key, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let cert_data = cert.clone().to_der().unwrap(); + let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); + assert_eq!(cert_data, data); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn mismatched_dcs_in_csr_and_cert() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + println!("Private Key is: {:?}", priv_key.key.to_bytes()); + println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); + println!(); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key, + &Capabilities::default_actor(), + Some(Target::Actor), + ) + .unwrap(); + let cert = IdCert::from_actor_csr( + csr, + &priv_key, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let cert_data = cert.clone().to_der().unwrap(); + let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); + assert_eq!(cert_data, data); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cert_from_pem() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key_actor = Ed25519PrivateKey::gen_keypair(&mut csprng); + let priv_key_home_server = Ed25519PrivateKey::gen_keypair(&mut csprng); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key_actor, + &Capabilities::default_actor(), + Some(Target::Actor), + ) + .unwrap(); + + let cert = IdCert::from_actor_csr( + csr, + &priv_key_home_server, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let cert_from_pem = IdCert::from_pem( + &data, + polyproto::certs::Target::Actor, + 10, + &priv_key_home_server.public_key, + ) + .unwrap(); + log::trace!( + "Cert from pem key usages: {:#?}", + cert_from_pem.id_cert_tbs.capabilities.key_usage.key_usages + ); + assert_eq!(cert_from_pem, cert); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + &priv_key_actor, + &Capabilities::default_home_server(), + Some(Target::HomeServer), + ) + .unwrap(); + let cert = IdCert::from_ca_csr( + csr, + &priv_key_home_server, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let cert_from_pem = IdCert::from_pem( + &data, + polyproto::certs::Target::Actor, + 10, + &priv_key_home_server.public_key, + ) + .unwrap(); + log::trace!( + "Cert from pem key usages: {:#?}", + cert_from_pem.id_cert_tbs.capabilities.key_usage.key_usages + ); + assert_eq!(cert_from_pem, cert); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cert_from_der() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key_actor = Ed25519PrivateKey::gen_keypair(&mut csprng); + let priv_key_home_server = Ed25519PrivateKey::gen_keypair(&mut csprng); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key_actor, + &Capabilities::default_actor(), + Some(Target::Actor), + ) + .unwrap(); + + let cert = IdCert::from_actor_csr( + csr, + &priv_key_home_server, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let data = cert.clone().to_der().unwrap(); + let cert_from_der = IdCert::from_der( + &data, + polyproto::certs::Target::Actor, + 10, + &priv_key_home_server.public_key, + ) + .unwrap(); + log::trace!( + "Cert from pem key usages: {:#?}", + cert_from_der.id_cert_tbs.capabilities.key_usage.key_usages + ); + assert_eq!(cert_from_der, cert); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + &priv_key_actor, + &Capabilities::default_home_server(), + Some(Target::HomeServer), + ) + .unwrap(); + let cert = IdCert::from_ca_csr( + csr, + &priv_key_home_server, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + Validity { + not_before: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), + ), + not_after: Time::UtcTime( + UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), + ), + }, + ) + .unwrap(); + let data = cert.clone().to_der().unwrap(); + let cert_from_der = IdCert::from_der( + &data, + polyproto::certs::Target::Actor, + 10, + &priv_key_home_server.public_key, + ) + .unwrap(); + log::trace!( + "Cert from pem key usages: {:#?}", + cert_from_der.id_cert_tbs.capabilities.key_usage.key_usages + ); + assert_eq!(cert_from_der, cert); +} diff --git a/tests/certs/idcsr.rs b/tests/certs/idcsr.rs new file mode 100644 index 0000000..189cd2b --- /dev/null +++ b/tests/certs/idcsr.rs @@ -0,0 +1,125 @@ +// Copyright (c) 2024 bitfl0wer +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#![allow(unused)] + +use std::str::FromStr; +use std::time::Duration; + +use crate::common::*; +use der::asn1::{BitString, Ia5String, Uint, UtcTime}; +use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; +use polyproto::certs::capabilities::{self, Capabilities}; +use polyproto::certs::idcert::IdCert; +use polyproto::certs::idcsr::IdCsr; +use polyproto::certs::{PublicKeyInfo, Target}; +use polyproto::key::{PrivateKey, PublicKey}; +use polyproto::signature::Signature; +use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; +use thiserror::Error; +use x509_cert::attr::Attributes; +use x509_cert::name::RdnSequence; +use x509_cert::request::CertReq; +use x509_cert::time::{Time, Validity}; +use x509_cert::Certificate; + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn csr_from_pem() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key, + &Capabilities::default_actor(), + Some(Target::Actor), + ) + .unwrap(); + let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let csr_from_pem = IdCsr::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); + assert_eq!(csr_from_pem, csr); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + &priv_key, + &Capabilities::default_home_server(), + Some(Target::HomeServer), + ) + .unwrap(); + let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let csr_from_pem = IdCsr::from_pem(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); + assert_eq!(csr_from_pem, csr); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn test_create_invalid_actor_csr() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + println!("Private Key is: {:?}", priv_key.key.to_bytes()); + println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); + println!(); + + let mut capabilities = Capabilities::default_actor(); + // This is not allowed in actor certificates/csrs + capabilities + .key_usage + .key_usages + .push(capabilities::KeyUsage::KeyCertSign); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key, + &capabilities, + Some(Target::Actor), + ); + assert!(csr.is_err()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn csr_from_der() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key, + &Capabilities::default_actor(), + Some(Target::Actor), + ) + .unwrap(); + let data = csr.clone().to_der().unwrap(); + let csr_from_der = IdCsr::from_der(&data, Some(polyproto::certs::Target::Actor)).unwrap(); + assert_eq!(csr_from_der, csr); + + let csr = polyproto::certs::idcsr::IdCsr::new( + &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), + &priv_key, + &Capabilities::default_home_server(), + Some(Target::HomeServer), + ) + .unwrap(); + let data = csr.clone().to_der().unwrap(); + let csr_from_der = IdCsr::from_der(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); + assert_eq!(csr_from_der, csr); +} diff --git a/tests/certs/mod.rs b/tests/certs/mod.rs new file mode 100644 index 0000000..c3052d7 --- /dev/null +++ b/tests/certs/mod.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod capabilities; +mod idcert; +mod idcsr; diff --git a/tests/idcert.rs b/tests/common/mod.rs similarity index 54% rename from tests/idcert.rs rename to tests/common/mod.rs index 5ea4559..3b94a76 100644 --- a/tests/idcert.rs +++ b/tests/common/mod.rs @@ -1,74 +1,62 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -#![allow(unused)] +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::str::FromStr; use std::time::Duration; -use der::asn1::{BitString, Ia5String, Uint, UtcTime}; -use der::Encode; -use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; -use polyproto::certs::capabilities::{self, Capabilities}; +use der::asn1::{BitString, Uint, UtcTime}; +use ed25519_dalek::ed25519::signature::Signer; +use ed25519_dalek::{Signature as Ed25519DalekSignature, SigningKey, VerifyingKey}; +use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; +use polyproto::certs::idcsr::IdCsr; use polyproto::certs::PublicKeyInfo; use polyproto::errors::composite::ConversionError; use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; +use polyproto::Name; use rand::rngs::OsRng; use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; -use thiserror::Error; -use x509_cert::attr::Attributes; -use x509_cert::name::RdnSequence; -use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; -use x509_cert::Certificate; -/// The following example uses the same setup as in ed25519_basic.rs, but in its main method, it -/// creates a certificate signing request (CSR) and writes it to a file. The CSR is created from a -/// polyproto ID CSR, which is a wrapper around a PKCS #10 CSR. -/// -/// If you have openssl installed, you can inspect the CSR by running: -/// -/// ```sh -/// openssl req -in cert.csr -verify -inform der -/// ``` -/// -/// After that, the program creates an ID-Cert from the given ID-CSR. The `cert.der` file can also -/// be validated using openssl: -/// -/// ```sh -/// openssl x509 -in cert.der -text -noout -inform der -/// ``` +pub fn init_logger() { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "trace"); + } + env_logger::builder().is_test(true).try_init().unwrap_or(()); +} -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] -fn test_create_actor_cert() { - let mut csprng = rand::rngs::OsRng; - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - println!("Private Key is: {:?}", priv_key.key.to_bytes()); - println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); - println!(); - let mut capabilities = Capabilities::default_actor(); - capabilities - .key_usage - .key_usages - .push(capabilities::KeyUsage::CrlSign); - let csr = polyproto::certs::idcsr::IdCsr::new( - &RdnSequence::from_str("CN=flori,DC=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), - &priv_key, - &capabilities, - ) - .unwrap(); - let cert = IdCert::from_actor_csr( - csr, +pub fn actor_subject(cn: &str) -> Name { + Name::from_str(&format!( + "CN={},DC=polyphony,DC=chat,UID={}@polyphony.chat,uniqueIdentifier=client1", + cn, cn + )) + .unwrap() +} + +pub fn default_validity() -> Validity { + Validity { + not_before: Time::UtcTime(UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap()), + not_after: Time::UtcTime(UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap()), + } +} + +pub fn home_server_subject() -> Name { + Name::from_str("DC=polyphony,DC=chat").unwrap() +} + +pub fn gen_priv_key() -> Ed25519PrivateKey { + Ed25519PrivateKey::gen_keypair(&mut rand::rngs::OsRng) +} + +pub fn actor_id_cert(cn: &str) -> IdCert { + let priv_key = gen_priv_key(); + IdCert::from_actor_csr( + actor_csr(cn, &priv_key), &priv_key, - Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,DC=www,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", - ) - .unwrap(), + Uint::new(&[8]).unwrap(), + home_server_subject(), Validity { not_before: Time::UtcTime( UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), @@ -78,35 +66,29 @@ fn test_create_actor_cert() { ), }, ) - .unwrap(); - let cert_data = cert.clone().to_der().unwrap(); - let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); - assert_eq!(cert_data, data); + .unwrap() } -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] -fn test_create_ca_cert() { - let mut csprng = rand::rngs::OsRng; - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - println!("Private Key is: {:?}", priv_key.key.to_bytes()); - println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); - println!(); - - let csr = polyproto::certs::idcsr::IdCsr::new( - &RdnSequence::from_str("CN=flori,DC=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), - &priv_key, - &Capabilities::default_home_server(), +pub fn actor_csr( + cn: &str, + priv_key: &Ed25519PrivateKey, +) -> IdCsr { + IdCsr::new( + &actor_subject(cn), + priv_key, + &Capabilities::default_actor(), + Some(polyproto::certs::Target::Actor), ) - .unwrap(); - let cert = IdCert::from_ca_csr( - csr, + .unwrap() +} + +pub fn home_server_id_cert() -> IdCert { + let priv_key = gen_priv_key(); + IdCert::from_ca_csr( + home_server_csr(&priv_key), &priv_key, - Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,DC=www,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", - ) - .unwrap(), + Uint::new(&[8]).unwrap(), + home_server_subject(), Validity { not_before: Time::UtcTime( UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), @@ -116,44 +98,29 @@ fn test_create_ca_cert() { ), }, ) - .unwrap(); - let cert_data = cert.clone().to_der().unwrap(); - let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); - assert_eq!(cert_data, data); + .unwrap() } -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] -fn test_create_invalid_actor_csr() { - let mut csprng = rand::rngs::OsRng; - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - println!("Private Key is: {:?}", priv_key.key.to_bytes()); - println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); - println!(); - - let mut capabilities = Capabilities::default_actor(); - // This is not allowed in actor certificates/csrs - capabilities - .key_usage - .key_usages - .push(capabilities::KeyUsage::KeyCertSign); - - let csr = polyproto::certs::idcsr::IdCsr::new( - &RdnSequence::from_str("CN=flori,DC=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), - &priv_key, - &capabilities, - ); - assert!(csr.is_err()); +pub fn home_server_csr(priv_key: &Ed25519PrivateKey) -> IdCsr { + IdCsr::new( + &home_server_subject(), + priv_key, + &Capabilities::default_home_server(), + Some(polyproto::certs::Target::HomeServer), + ) + .unwrap() } -// As mentioned in the README, we start by implementing the signature trait. - -// Here, we start by defining the signature type, which is a wrapper around the signature type from -// the ed25519-dalek crate. #[derive(Debug, PartialEq, Eq, Clone)] -struct Ed25519Signature { - signature: Ed25519DalekSignature, - algorithm: AlgorithmIdentifierOwned, +pub(crate) struct Ed25519Signature { + pub(crate) signature: Ed25519DalekSignature, + pub(crate) algorithm: AlgorithmIdentifierOwned, +} + +impl std::fmt::Display for Ed25519Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.signature) + } } // We implement the Signature trait for our signature type. @@ -177,7 +144,7 @@ impl Signature for Ed25519Signature { } } - fn from_bitstring(signature: &[u8]) -> Self { + fn from_bytes(signature: &[u8]) -> Self { let mut signature_vec = signature.to_vec(); signature_vec.resize(64, 0); let signature_array: [u8; 64] = { @@ -202,11 +169,11 @@ impl SignatureBitStringEncoding for Ed25519Signature { // Next, we implement the key traits. We start by defining the private key type. #[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PrivateKey { +pub(crate) struct Ed25519PrivateKey { // Defined below - public_key: Ed25519PublicKey, + pub(crate) public_key: Ed25519PublicKey, // The private key from the ed25519-dalek crate - key: SigningKey, + pub(crate) key: SigningKey, } impl PrivateKey for Ed25519PrivateKey { @@ -241,9 +208,9 @@ impl Ed25519PrivateKey { // Same thing as above for the public key type. #[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PublicKey { +pub(crate) struct Ed25519PublicKey { // The public key type from the ed25519-dalek crate - key: VerifyingKey, + pub(crate) key: VerifyingKey, } impl PublicKey for Ed25519PublicKey { diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 0000000..3a16bbd --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub(crate) mod api; +pub(crate) mod certs; +pub(crate) mod common; + +use polyproto::Constrained;