From d9ee44bb8182902cfed2abe747499939a6a3c851 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 18:28:01 +0200 Subject: [PATCH 001/215] add API feature --- .vscode/settings.json | 3 ++- Cargo.toml | 12 +++++++----- src/api/mod.rs | 3 +++ src/lib.rs | 3 +++ 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/api/mod.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c6d107..480c6cb 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": ["api"] } diff --git a/Cargo.toml b/Cargo.toml index 28825b9..d36c92b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,15 @@ crate-type = ["rlib", "cdylib", "staticlib"] [features] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] +api = ["dep:reqwest"] [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" +reqwest = { version = "0.12.3", features = ["json"], optional = true } spki = "0.7.3" -thiserror = "1.0.57" +thiserror = "1.0.58" x509-cert = { version = "0.2.5", default-features = false } [dev-dependencies] @@ -28,5 +30,5 @@ rand = "0.8.5" polyproto = { path = "./" } [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/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..7be716e --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,3 @@ +// 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/. diff --git a/src/lib.rs b/src/lib.rs index b5118f8..3fcc4dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,9 @@ pub mod signature; /// Error types used in this crate pub mod errors; +#[cfg(feature = "api")] +pub mod api; + pub(crate) mod constraints; pub use der; From 6282321cb0ddc1d03c6e00e314555fa89ac4c758 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 18:34:36 +0200 Subject: [PATCH 002/215] create basic file structure --- src/api/authentication/mod.rs | 3 +++ src/api/events/mod.rs | 3 +++ src/api/identity/mod.rs | 3 +++ src/api/mod.rs | 5 +++++ src/api/types/mod.rs | 3 +++ 5 files changed, 17 insertions(+) create mode 100644 src/api/authentication/mod.rs create mode 100644 src/api/events/mod.rs create mode 100644 src/api/identity/mod.rs create mode 100644 src/api/types/mod.rs diff --git a/src/api/authentication/mod.rs b/src/api/authentication/mod.rs new file mode 100644 index 0000000..7be716e --- /dev/null +++ b/src/api/authentication/mod.rs @@ -0,0 +1,3 @@ +// 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/. diff --git a/src/api/events/mod.rs b/src/api/events/mod.rs new file mode 100644 index 0000000..7be716e --- /dev/null +++ b/src/api/events/mod.rs @@ -0,0 +1,3 @@ +// 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/. diff --git a/src/api/identity/mod.rs b/src/api/identity/mod.rs new file mode 100644 index 0000000..7be716e --- /dev/null +++ b/src/api/identity/mod.rs @@ -0,0 +1,3 @@ +// 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/. diff --git a/src/api/mod.rs b/src/api/mod.rs index 7be716e..2722cc1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,8 @@ // 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 mod authentication; +pub mod events; +pub mod identity; +pub mod types; diff --git a/src/api/types/mod.rs b/src/api/types/mod.rs new file mode 100644 index 0000000..7be716e --- /dev/null +++ b/src/api/types/mod.rs @@ -0,0 +1,3 @@ +// 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/. From 1cca784dd58637a75d08078aaa64415349e9e171 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:16:36 +0200 Subject: [PATCH 003/215] Add serde, base64 features --- Cargo.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d36c92b..7d26816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,18 @@ crate-type = ["rlib", "cdylib", "staticlib"] [features] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] -api = ["dep:reqwest"] +api = ["dep:reqwest", "serde", "base64"] +serde = ["dep:serde", "dep:serde_json"] +base64 = ["dep:base64"] [dependencies] +base64 = { version = "0.22.0", optional = true } der = { version = "0.7.9", features = ["pem"] } getrandom = { version = "0.2.14", optional = true } regex = "1.10.4" reqwest = { version = "0.12.3", features = ["json"], optional = true } +serde = { version = "1.0.198", optional = true, features = ["derive"] } +serde_json = { version = "1.0.116", optional = true } spki = "0.7.3" thiserror = "1.0.58" x509-cert = { version = "0.2.5", default-features = false } From cca7b2872cddc9c3441ede587ddea1a55bd7bd1c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:48:35 +0200 Subject: [PATCH 004/215] from_pem, to_pem for IdCsr, IdCert --- src/certs/idcert.rs | 15 ++++++++++++++- src/certs/idcsr.rs | 11 ++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 2c0eda0..818e7d0 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -3,7 +3,8 @@ // 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; @@ -119,6 +120,18 @@ impl> IdCert { pub fn to_der(self) -> Result, ConversionError> { Ok(Certificate::try_from(self)?.to_der()?) } + + /// Create an IdCsr from a byte slice containing a PEM encoded X.509 Certificate. + pub fn from_pem(pem: &str) -> Result { + let cert = IdCert::try_from(Certificate::from_pem(pem)?)?; + cert.validate()?; + 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)?) + } } impl> TryFrom> for Certificate { diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index dec3670..df0613c 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -4,7 +4,8 @@ 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; @@ -111,6 +112,14 @@ impl> IdCsr { pub fn to_der(self) -> Result, ConversionError> { Ok(CertReq::try_from(self)?.to_der()?) } + + pub fn from_pem(pem: &str) -> Result { + IdCsr::try_from(CertReq::from_pem(pem)?) + } + + pub fn to_pem(self, line_ending: LineEnding) -> Result { + Ok(CertReq::try_from(self)?.to_pem(line_ending)?) + } } /// In the context of PKCS #10, this is a `CertificationRequestInfo`: From 0d136d63976a99c0586d3c196c0eb584b1165873 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:48:48 +0200 Subject: [PATCH 005/215] Make Constrained pub trait --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3fcc4dd..464a702 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ pub use spki; /// 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 { +pub trait Constrained { fn validate(&self) -> Result<(), ConstraintError>; } From 82a942cdf675393656a4ba424a67344456ad53e6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:50:08 +0200 Subject: [PATCH 006/215] Bump versions, remove base64, add x509_cert:pem --- Cargo.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d26816..053164e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,11 @@ crate-type = ["rlib", "cdylib", "staticlib"] [features] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] -api = ["dep:reqwest", "serde", "base64"] +api = ["dep:reqwest", "serde"] serde = ["dep:serde", "dep:serde_json"] -base64 = ["dep:base64"] [dependencies] -base64 = { version = "0.22.0", optional = true } -der = { version = "0.7.9", features = ["pem"] } +der = "0.7.9" getrandom = { version = "0.2.14", optional = true } regex = "1.10.4" reqwest = { version = "0.12.3", features = ["json"], optional = true } @@ -27,7 +25,7 @@ serde = { version = "1.0.198", optional = true, features = ["derive"] } serde_json = { version = "1.0.116", optional = true } spki = "0.7.3" thiserror = "1.0.58" -x509-cert = { version = "0.2.5", default-features = false } +x509-cert = { version = "0.2.5", default-features = false, features = ["pem"] } [dev-dependencies] ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } From f7fae073747af7ba7308b0eb58344ffad0d9366f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:51:43 +0200 Subject: [PATCH 007/215] Add schemas --- src/api/types/mod.rs | 2 ++ src/api/types/schemas/authentication.rs | 31 +++++++++++++++++++++++++ src/api/types/schemas/mod.rs | 5 ++++ 3 files changed, 38 insertions(+) create mode 100644 src/api/types/schemas/authentication.rs create mode 100644 src/api/types/schemas/mod.rs diff --git a/src/api/types/mod.rs b/src/api/types/mod.rs index 7be716e..4c4e25b 100644 --- a/src/api/types/mod.rs +++ b/src/api/types/mod.rs @@ -1,3 +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/. + +pub mod schemas; diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs new file mode 100644 index 0000000..580e26d --- /dev/null +++ b/src/api/types/schemas/authentication.rs @@ -0,0 +1,31 @@ +// 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::certs::idcert::IdCert; +use crate::errors::composite::ConversionError; +use crate::key::PublicKey; +use crate::signature::Signature; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct NewIdCert { + pub actor_name: String, + pub csr: String, + pub auth_payload: Option, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct NewIdCertCreated { + pub id_cert: String, + pub token: String, +} + +impl> TryFrom for IdCert { + type Error = ConversionError; + + fn try_from(value: NewIdCertCreated) -> Result { + todo!() + } +} diff --git a/src/api/types/schemas/mod.rs b/src/api/types/schemas/mod.rs new file mode 100644 index 0000000..b05c34b --- /dev/null +++ b/src/api/types/schemas/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/. + +pub mod authentication; From 95f3de9fcf2f1bfcc99dca1c6e3faa70ae0844b6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:53:03 +0200 Subject: [PATCH 008/215] impl> TryFrom for IdCert --- src/api/types/schemas/authentication.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs index 580e26d..8f78876 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/api/types/schemas/authentication.rs @@ -26,6 +26,6 @@ impl> TryFrom for IdCert { type Error = ConversionError; fn try_from(value: NewIdCertCreated) -> Result { - todo!() + Self::from_pem(&value.id_cert) } } From aa1a0e736c5262bc1b162d7624ba242ce7aa1251 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 19:54:33 +0200 Subject: [PATCH 009/215] rename auth schemas --- src/api/types/schemas/authentication.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs index 8f78876..36572b7 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/api/types/schemas/authentication.rs @@ -9,7 +9,7 @@ use crate::signature::Signature; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct NewIdCert { +pub struct IdCertCreationSchema { pub actor_name: String, pub csr: String, pub auth_payload: Option, @@ -17,15 +17,15 @@ pub struct NewIdCert { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct NewIdCertCreated { +pub struct IdCertCreatedSchema { pub id_cert: String, pub token: String, } -impl> TryFrom for IdCert { +impl> TryFrom for IdCert { type Error = ConversionError; - fn try_from(value: NewIdCertCreated) -> Result { + fn try_from(value: IdCertCreatedSchema) -> Result { Self::from_pem(&value.id_cert) } } From 8cfd9d0b647d0fc07d694381eb2471404fbaa338 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 20:29:18 +0200 Subject: [PATCH 010/215] Rename feature api to routes --- .vscode/settings.json | 2 +- Cargo.toml | 2 +- src/lib.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 480c6cb..182ed5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "markiscodecoverage.searchCriteria": ".coverage/lcov*.info", - "rust-analyzer.cargo.features": ["api"] + "rust-analyzer.cargo.features": ["routes"] } diff --git a/Cargo.toml b/Cargo.toml index 053164e..90f032e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["rlib", "cdylib", "staticlib"] [features] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] -api = ["dep:reqwest", "serde"] +routes = ["dep:reqwest", "serde"] serde = ["dep:serde", "dep:serde_json"] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index 464a702..b438cbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,7 +72,6 @@ pub mod signature; /// Error types used in this crate pub mod errors; -#[cfg(feature = "api")] pub mod api; pub(crate) mod constraints; From 3e1e025df000e285ede0c8147e35c4f98cf04633 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 20:29:36 +0200 Subject: [PATCH 011/215] Rename IdCertCreationSchema, IdCertCreatedSchema --- src/api/types/schemas/authentication.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs index 36572b7..58045fb 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/api/types/schemas/authentication.rs @@ -9,7 +9,7 @@ use crate::signature::Signature; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct IdCertCreationSchema { +pub struct CreateSessionSchema { pub actor_name: String, pub csr: String, pub auth_payload: Option, @@ -17,15 +17,15 @@ pub struct IdCertCreationSchema { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct IdCertCreatedSchema { +pub struct SessionCreatedSchema { pub id_cert: String, pub token: String, } -impl> TryFrom for IdCert { +impl> TryFrom for IdCert { type Error = ConversionError; - fn try_from(value: IdCertCreatedSchema) -> Result { + fn try_from(value: SessionCreatedSchema) -> Result { Self::from_pem(&value.id_cert) } } From d2e973d523599933316ea4f2f19ffa72bc590b92 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 20:29:59 +0200 Subject: [PATCH 012/215] Implement Challenge type --- src/api/types/entities/challenge_string.rs | 66 ++++++++++++++++++++++ src/api/types/entities/mod.rs | 7 +++ src/api/types/mod.rs | 1 + 3 files changed, 74 insertions(+) create mode 100644 src/api/types/entities/challenge_string.rs create mode 100644 src/api/types/entities/mod.rs diff --git a/src/api/types/entities/challenge_string.rs b/src/api/types/entities/challenge_string.rs new file mode 100644 index 0000000..428869a --- /dev/null +++ b/src/api/types/entities/challenge_string.rs @@ -0,0 +1,66 @@ +// 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::asn1::Ia5String; +use der::Length; + +use crate::errors::base::ConstraintError; +use crate::errors::composite::ConversionError; +use crate::Constrained; + +pub struct Challenge { + challenge: Ia5String, +} + +impl std::str::FromStr for Challenge { + type Err = ConversionError; + + fn from_str(s: &str) -> Result { + let challenge = Self { + challenge: Ia5String::new(s)?, + }; + challenge.validate()?; + Ok(challenge) + } +} + +impl Deref for Challenge { + type Target = Ia5String; + + fn deref(&self) -> &Self::Target { + &self.challenge + } +} + +impl DerefMut for Challenge { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.challenge + } +} + +impl Constrained for Challenge { + fn validate(&self) -> Result<(), crate::errors::base::ConstraintError> { + if self.challenge.len() < Length::new(32) { + return Err(ConstraintError::OutOfBounds { + lower: 32, + upper: 256, + actual: self.challenge.len().to_string(), + reason: "Challenge string must be at least 32 characters long".to_string(), + }); + } + + if self.challenge.len() > Length::new(256) { + return Err(ConstraintError::OutOfBounds { + lower: 32, + upper: 256, + actual: self.challenge.len().to_string(), + reason: "Challenge string must be at most 256 characters long".to_string(), + }); + } + + Ok(()) + } +} diff --git a/src/api/types/entities/mod.rs b/src/api/types/entities/mod.rs new file mode 100644 index 0000000..ed41408 --- /dev/null +++ b/src/api/types/entities/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/. + +pub mod challenge_string; + +pub use challenge_string::*; diff --git a/src/api/types/mod.rs b/src/api/types/mod.rs index 4c4e25b..78b9bde 100644 --- a/src/api/types/mod.rs +++ b/src/api/types/mod.rs @@ -2,4 +2,5 @@ // 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 mod entities; pub mod schemas; From f08c367d238891d5317e6c8ab34924b3b30553f4 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 20:30:31 +0200 Subject: [PATCH 013/215] Add missing documentation for associated functions --- src/certs/idcsr.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index df0613c..04297c5 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -113,10 +113,12 @@ impl> IdCsr { Ok(CertReq::try_from(self)?.to_der()?) } + /// Create an IdCsr from a string containing a PEM encoded PKCS #10 CSR. pub fn from_pem(pem: &str) -> Result { IdCsr::try_from(CertReq::from_pem(pem)?) } + /// 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)?) } From 70ce2b98370c759158af1f6fdb57357903643a1d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 20:44:55 +0200 Subject: [PATCH 014/215] impl Challenge over impl std::str::FromStr for Challenge --- src/api/types/entities/challenge_string.rs | 24 ++++++++++++++-------- src/api/types/schemas/authentication.rs | 16 +++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/api/types/entities/challenge_string.rs b/src/api/types/entities/challenge_string.rs index 428869a..c4fa3a0 100644 --- a/src/api/types/entities/challenge_string.rs +++ b/src/api/types/entities/challenge_string.rs @@ -11,19 +11,25 @@ use crate::errors::base::ConstraintError; use crate::errors::composite::ConversionError; use crate::Constrained; +#[derive(Debug, Clone, PartialEq, Eq)] +/// A challenge string, used to prove that an actor possesses a private key, without revealing it. pub struct Challenge { challenge: Ia5String, + pub expires: u64, } -impl std::str::FromStr for Challenge { - type Err = ConversionError; - - fn from_str(s: &str) -> Result { - let challenge = Self { - challenge: Ia5String::new(s)?, - }; - challenge.validate()?; - Ok(challenge) +impl Challenge { + /// Creates a new challenge string. + /// + /// ## Arguments + /// + /// - **challenge**: The challenge string. + /// - **expires**: The UNIX timestamp when the challenge expires. + pub fn new(challenge: &str, expires: u64) -> Result { + Ok(Self { + challenge: Ia5String::new(challenge)?, + expires, + }) } } diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs index 58045fb..23539c4 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/api/types/schemas/authentication.rs @@ -2,6 +2,7 @@ // 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::api::types::entities::Challenge; use crate::certs::idcert::IdCert; use crate::errors::composite::ConversionError; use crate::key::PublicKey; @@ -29,3 +30,18 @@ impl> TryFrom for IdCert for Challenge { + type Error = ConversionError; + + fn try_from(value: GetChallengeResponseSchema) -> Result { + Challenge::new(&value.challenge, value.expires) + } +} From b7e86320f2b954771df58d88ad205912f3df46bb Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 20:51:24 +0200 Subject: [PATCH 015/215] Implement complete() for Challenge --- src/api/types/entities/challenge_string.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/api/types/entities/challenge_string.rs b/src/api/types/entities/challenge_string.rs index c4fa3a0..27f0b35 100644 --- a/src/api/types/entities/challenge_string.rs +++ b/src/api/types/entities/challenge_string.rs @@ -9,6 +9,8 @@ use der::Length; use crate::errors::base::ConstraintError; use crate::errors::composite::ConversionError; +use crate::key::PrivateKey; +use crate::signature::Signature; use crate::Constrained; #[derive(Debug, Clone, PartialEq, Eq)] @@ -31,6 +33,11 @@ impl Challenge { expires, }) } + + /// Completes the challenge by signing it with the private key. + pub fn complete>(&self, key: &V) -> S { + key.sign(self.challenge.as_bytes()) + } } impl Deref for Challenge { From 6ae96d168f72b43d4b5d8785405c1786ccb0641a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 21:48:31 +0200 Subject: [PATCH 016/215] Expand dictionary --- .vscode/ltex.dictionary.en-US.txt | 1 + .vscode/ltex.hiddenFalsePositives.en-US.txt | 3 +++ 2 files changed, 4 insertions(+) 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..e823cb9 100644 --- a/.vscode/ltex.hiddenFalsePositives.en-US.txt +++ b/.vscode/ltex.hiddenFalsePositives.en-US.txt @@ -15,3 +15,6 @@ {"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$"} From 26a8ed32c290aedfdf5f1aeb89946bde9559945d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 21:48:49 +0200 Subject: [PATCH 017/215] Add ToString requirement for Signature trait --- examples/ed25519_basic.rs | 6 ++++++ examples/ed25519_cert.rs | 6 ++++++ examples/ed25519_from_der.rs | 6 ++++++ src/signature.rs | 2 +- tests/idcert.rs | 6 ++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/examples/ed25519_basic.rs b/examples/ed25519_basic.rs index 2c6ff7f..8684489 100644 --- a/examples/ed25519_basic.rs +++ b/examples/ed25519_basic.rs @@ -74,6 +74,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. diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index be2d7f2..8e570ad 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -99,6 +99,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. diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index 5a7c3f6..b228ae2 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -92,6 +92,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. diff --git a/src/signature.rs b/src/signature.rs index 6f0675e..492fa23 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -5,7 +5,7 @@ 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 { type Signature; /// The signature value fn as_signature(&self) -> &Self::Signature; diff --git a/tests/idcert.rs b/tests/idcert.rs index 5ea4559..c4a9377 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -156,6 +156,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. From 6726465a39b003f1f7e72ecc5c3c94a40db2bce9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 21:49:21 +0200 Subject: [PATCH 018/215] Add CompletedChallenge type, make Challenge attributes AT LEAST pub(crate) --- src/api/types/entities/challenge_string.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/api/types/entities/challenge_string.rs b/src/api/types/entities/challenge_string.rs index 27f0b35..3fada3f 100644 --- a/src/api/types/entities/challenge_string.rs +++ b/src/api/types/entities/challenge_string.rs @@ -16,7 +16,7 @@ use crate::Constrained; #[derive(Debug, Clone, PartialEq, Eq)] /// A challenge string, used to prove that an actor possesses a private key, without revealing it. pub struct Challenge { - challenge: Ia5String, + pub(crate) challenge: Ia5String, pub expires: u64, } @@ -35,8 +35,12 @@ impl Challenge { } /// Completes the challenge by signing it with the private key. - pub fn complete>(&self, key: &V) -> S { - key.sign(self.challenge.as_bytes()) + pub fn complete>(&self, key: &V) -> CompletedChallenge { + let s = key.sign(self.challenge.as_bytes()); + CompletedChallenge { + challenge: self.clone(), + signature: s, + } } } @@ -77,3 +81,11 @@ impl Constrained for Challenge { Ok(()) } } + +/// A completed challenge, containing the challenge and the signature. +pub struct CompletedChallenge { + /// The challenge. + pub challenge: Challenge, + /// The signature of the challenge. + pub signature: S, +} From 7a8ecd12706eb95e8941f0ca3fb133e55d77dcf7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 21:49:52 +0200 Subject: [PATCH 019/215] Add new schemas, document things --- src/api/types/schemas/authentication.rs | 73 ++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs index 23539c4..886e1b2 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/api/types/schemas/authentication.rs @@ -2,7 +2,7 @@ // 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::api::types::entities::Challenge; +use crate::api::types::entities::{Challenge, CompletedChallenge}; use crate::certs::idcert::IdCert; use crate::errors::composite::ConversionError; use crate::key::PublicKey; @@ -10,38 +10,95 @@ use crate::signature::Signature; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct CreateSessionSchema { +/// Schema for creating a new session. +/// +/// `/p2core/session/trust` +pub struct CreateSessionRequest { + /// The name of the actor that is creating the session. pub actor_name: String, + /// PEM encoded [IdCsr] pub csr: String, + /// Optional authentication payload. pub auth_payload: Option, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct SessionCreatedSchema { +/// Response counterpart of [CreateSessionSchema]. +pub struct CreateSessionResponse { + /// PEM encoded [IdCert] pub id_cert: String, + /// An authentication (bearer) token. pub token: String, } -impl> TryFrom for IdCert { +impl> TryFrom for IdCert { type Error = ConversionError; - fn try_from(value: SessionCreatedSchema) -> Result { + fn try_from(value: CreateSessionResponse) -> Result { Self::from_pem(&value.id_cert) } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -pub struct GetChallengeResponseSchema { +/// Schema for getting a challenge. Can be converted to [Challenge] using [TryFrom]. +/// +/// `/p2core/challenge` +pub struct GetChallengeResponse { + /// The challenge string. pub challenge: String, + /// UNIX timestamp when the challenge expires. pub expires: u64, } -impl TryFrom for Challenge { +impl TryFrom for Challenge { type Error = ConversionError; - fn try_from(value: GetChallengeResponseSchema) -> Result { + fn try_from(value: GetChallengeResponse) -> Result { Challenge::new(&value.challenge, value.expires) } } + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +/// Completed challenge payload. +pub struct ChallengePayload { + /// The challenge string. + pub challenge: String, + /// The signature of the challenge. + pub signature: String, +} + +impl From> for ChallengePayload { + fn from(value: CompletedChallenge) -> Self { + Self { + challenge: value.challenge.to_string(), + signature: value.signature.to_string(), + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +/// Identify payload to log a session in. +/// +/// `/p2core/session/identify` +pub struct IdentifyRequest { + /// A completed challenge. + pub challenge_signature: ChallengePayload, + /// PEM encoded [IdCert] + pub id_cert: String, + /// Optional authentication payload. + pub auth_payload: Option, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +/// Response counterpart of [IdentifyRequest]. +pub struct IdentifyResponse { + /// An authentication (bearer) token. + pub token: String, + /// Optional payload from the server. + pub payload: Option, +} From 74d2612492b73d2632dd4405f8ba0ecad2cdd2db Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 18 Apr 2024 21:50:22 +0200 Subject: [PATCH 020/215] add todo --- src/api/types/schemas/authentication.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/types/schemas/authentication.rs b/src/api/types/schemas/authentication.rs index 886e1b2..17fb905 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/api/types/schemas/authentication.rs @@ -63,6 +63,7 @@ impl TryFrom for Challenge { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] /// Completed challenge payload. +// TODO: Move this to /types/entities or another, more appropriate module. pub struct ChallengePayload { /// The challenge string. pub challenge: String, From 4201cbf8b2cd927621e766fba642a625796e28e7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 19 Apr 2024 17:55:12 +0200 Subject: [PATCH 021/215] Add FireDBG support --- firedbg/target-unit-test.json | 6 ++++++ firedbg/target.json | 36 +++++++++++++++++++++++++++++++++++ firedbg/version.toml | 1 + 3 files changed, 43 insertions(+) create mode 100644 firedbg/target-unit-test.json create mode 100644 firedbg/target.json create mode 100644 firedbg/version.toml 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" From 183b709cd63efb0815c0fdfebb608978efbb7612 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 19 Apr 2024 18:00:36 +0200 Subject: [PATCH 022/215] add firedbg dir to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 9b1d6171cbf75e0905284c7c96b4193d36f9c66b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 29 Apr 2024 15:41:07 +0200 Subject: [PATCH 023/215] Add reqwest feature --- Cargo.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90f032e..7dbb8e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,20 +11,22 @@ rust-version = "1.65.0" crate-type = ["rlib", "cdylib", "staticlib"] [features] +default = ["routes", "reqwest"] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] -routes = ["dep:reqwest", "serde"] +routes = ["serde"] +reqwest = ["dep:reqwest"] serde = ["dep:serde", "dep:serde_json"] [dependencies] der = "0.7.9" getrandom = { version = "0.2.14", optional = true } regex = "1.10.4" -reqwest = { version = "0.12.3", features = ["json"], optional = true } -serde = { version = "1.0.198", optional = true, features = ["derive"] } +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 = "0.7.3" -thiserror = "1.0.58" +thiserror = "1.0.59" x509-cert = { version = "0.2.5", default-features = false, features = ["pem"] } [dev-dependencies] From 9e426058351cd729cc03ee1049497d6709589062 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 29 Apr 2024 15:43:37 +0200 Subject: [PATCH 024/215] Add reqwest note to README --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d028e97..dafbcb0 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,28 @@ implementing polyproto by transforming the [polyproto specification](https://docs.polyphony.chat/Protocol%20Specifications/core/) into well-defined yet adaptable Rust types. +```toml +[dependencies] +polyproto = { version = "0", features = ["wasm"] } +``` + ## WebAssembly This crate is designed to work with the `wasm32-unknown-unknown` target. To compile for `wasm`, you will have to use the `wasm` feature: +## reqwest + +By default, this crate uses `reqwest` for HTTP requests. `reqwest` is an optional dependency, and +you can disable it by using polyproto with `default-features = false` in your `Cargo.toml`: + ```toml [dependencies] -polyproto = { version = "0", features = ["wasm"] } +polyproto = { version = "0", default-features = false, features = ["routes"] } ``` +Disabling `reqwest` gives you the ability to use your own HTTP client. + [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 From 74d39c8a53befa139802078726fccaf895d25a63 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 29 Apr 2024 20:26:20 +0200 Subject: [PATCH 025/215] move types to src/ --- src/api/mod.rs | 1 - src/lib.rs | 23 +++++++------------ .../types/entities/challenge_string.rs | 0 src/{api => }/types/entities/mod.rs | 0 src/{api => }/types/mod.rs | 0 src/{api => }/types/schemas/authentication.rs | 2 +- src/{api => }/types/schemas/mod.rs | 0 7 files changed, 9 insertions(+), 17 deletions(-) rename src/{api => }/types/entities/challenge_string.rs (100%) rename src/{api => }/types/entities/mod.rs (100%) rename src/{api => }/types/mod.rs (100%) rename src/{api => }/types/schemas/authentication.rs (98%) rename src/{api => }/types/schemas/mod.rs (100%) diff --git a/src/api/mod.rs b/src/api/mod.rs index 2722cc1..ec56e5b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,4 +5,3 @@ pub mod authentication; pub mod events; pub mod identity; -pub mod types; diff --git a/src/lib.rs b/src/lib.rs index b438cbe..6afce36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,6 @@ will have to use the `wasm` feature: [dependencies] polyproto = { version = "0", features = ["wasm"] } ``` - */ pub const OID_RDN_DOMAIN_COMPONENT: &str = "0.9.2342.19200300.100.1.25"; @@ -53,26 +52,20 @@ pub const OID_RDN_UID: &str = "0.9.2342.19200300.100.1.1"; 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; - -/// Error types used in this crate -pub mod errors; - -pub mod api; +#[cfg(feature = "routes")] +/// Types used in polyproto and the polyproto HTTP/REST APIs +pub mod types; pub(crate) mod constraints; diff --git a/src/api/types/entities/challenge_string.rs b/src/types/entities/challenge_string.rs similarity index 100% rename from src/api/types/entities/challenge_string.rs rename to src/types/entities/challenge_string.rs diff --git a/src/api/types/entities/mod.rs b/src/types/entities/mod.rs similarity index 100% rename from src/api/types/entities/mod.rs rename to src/types/entities/mod.rs diff --git a/src/api/types/mod.rs b/src/types/mod.rs similarity index 100% rename from src/api/types/mod.rs rename to src/types/mod.rs diff --git a/src/api/types/schemas/authentication.rs b/src/types/schemas/authentication.rs similarity index 98% rename from src/api/types/schemas/authentication.rs rename to src/types/schemas/authentication.rs index 17fb905..df9451e 100644 --- a/src/api/types/schemas/authentication.rs +++ b/src/types/schemas/authentication.rs @@ -2,11 +2,11 @@ // 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::api::types::entities::{Challenge, CompletedChallenge}; use crate::certs::idcert::IdCert; use crate::errors::composite::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; +use crate::types::entities::{Challenge, CompletedChallenge}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] diff --git a/src/api/types/schemas/mod.rs b/src/types/schemas/mod.rs similarity index 100% rename from src/api/types/schemas/mod.rs rename to src/types/schemas/mod.rs From e31638baae0136bc74033e149406ad380aa6644a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 4 May 2024 12:44:31 +0200 Subject: [PATCH 026/215] start impl HttpClient --- src/api/mod.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index ec56e5b..3b4f324 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,6 +2,62 @@ // 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; + pub mod authentication; pub mod events; pub mod identity; + +#[derive(Debug, Default, Clone)] +pub struct HttpClient { + client: reqwest::Client, + headers: reqwest::header::HeaderMap, +} + +impl HttpClient { + pub fn new() -> Self { + let client = reqwest::Client::new(); + let headers = reqwest::header::HeaderMap::new(); + Self { client, headers } + } + + pub fn with_headers(mut self, headers: reqwest::header::HeaderMap) -> Self { + self.headers = headers; + self + } + + pub async fn request( + &self, + method: reqwest::Method, + url: &str, + body: Option, + ) -> Result { + let mut request = self.client.request(method, url); + request = request.headers(self.headers.clone()); + if let Some(body) = body { + request = request.body(body); + } + request.send().await + } + + /// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains a [`T`] if the request + /// was successful, or a [`ChorusError`] if the request failed. + pub(crate) async fn handle_response Deserialize<'a>>( + response: Result, + ) -> Result { + let response = match response { + Ok(response) => response, + Err(e) => return todo!(), + }; + let response_text = match response.text().await { + Ok(string) => string, + Err(e) => return todo!(), + }; + let object = match from_str::(&response_text) { + Ok(object) => object, + Err(e) => return todo!(), + }; + Ok(object) + } +} From b5866c432d58713057a69eea62aad7f47769260c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 5 May 2024 00:05:49 +0200 Subject: [PATCH 027/215] Implement handle_request into HttpClient --- src/api/mod.rs | 24 +++++++++--------------- src/errors/composite.rs | 8 ++++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 3b4f324..00363b2 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,6 +5,8 @@ use serde::Deserialize; use serde_json::from_str; +use crate::errors::composite::RequestError; + pub mod authentication; pub mod events; pub mod identity; @@ -22,11 +24,13 @@ impl HttpClient { Self { client, headers } } + /// Creates a new instance of the client with the provided headers. pub fn with_headers(mut self, headers: reqwest::header::HeaderMap) -> Self { self.headers = headers; self } + /// Sends a request and returns the response. pub async fn request( &self, method: reqwest::Method, @@ -41,23 +45,13 @@ impl HttpClient { request.send().await } - /// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains a [`T`] if the request - /// was successful, or a [`ChorusError`] if the request failed. + /// Sends a request, handles the response, and returns the deserialized object. pub(crate) async fn handle_response Deserialize<'a>>( response: Result, - ) -> Result { - let response = match response { - Ok(response) => response, - Err(e) => return todo!(), - }; - let response_text = match response.text().await { - Ok(string) => string, - Err(e) => return todo!(), - }; - let object = match from_str::(&response_text) { - Ok(object) => object, - Err(e) => return todo!(), - }; + ) -> Result { + let response = response?; + let response_text = response.text().await?; + let object = from_str::(&response_text)?; Ok(object) } } diff --git a/src/errors/composite.rs b/src/errors/composite.rs index 9172408..52527b2 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -45,6 +45,14 @@ pub enum ConversionError { IdCertError(#[from] PublicKeyError), } +#[derive(Error, Debug)] +pub enum RequestError { + #[error(transparent)] + HttpError(#[from] reqwest::Error), + #[error("Failed to deserialize response into expected type")] + DeserializationError(#[from] serde_json::Error), +} + impl From for ConversionError { fn from(value: der::Error) -> Self { Self::DerError(value) From 2876c7920dd4ec927744cd20fbe8cf7779f42b9a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 5 May 2024 17:30:19 +0200 Subject: [PATCH 028/215] Implement "Get Challenge" route --- Cargo.toml | 5 +++-- src/api/identity/mod.rs | 20 ++++++++++++++++++++ src/api/mod.rs | 2 ++ src/types/entities/challenge_string.rs | 8 +++++--- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7dbb8e5..f1cc77c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ default = ["routes", "reqwest"] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] routes = ["serde"] -reqwest = ["dep:reqwest"] -serde = ["dep:serde", "dep:serde_json"] +reqwest = ["dep:reqwest", "routes"] +serde = ["dep:serde", "dep:serde_json", "dep:ser_der"] [dependencies] der = "0.7.9" @@ -28,6 +28,7 @@ serde_json = { version = "1.0.116", optional = true } spki = "0.7.3" thiserror = "1.0.59" x509-cert = { version = "0.2.5", default-features = false, features = ["pem"] } +ser_der = { version = "0.1.0-alpha.1", optional = true, features = ["alloc"] } [dev-dependencies] ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } diff --git a/src/api/identity/mod.rs b/src/api/identity/mod.rs index 7be716e..d8b6ae0 100644 --- a/src/api/identity/mod.rs +++ b/src/api/identity/mod.rs @@ -1,3 +1,23 @@ // 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 reqwest::Method; + +use crate::types::entities::Challenge; + +use super::{HttpClient, HttpResult}; + +impl HttpClient { + pub async fn get_challenge(&self, base_url: &str) -> HttpResult { + let response = self + .request( + Method::GET, + &format!("{}/.p2/core/v1/challenge", base_url), + None, + ) + .await; + + Self::handle_response(response).await + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 00363b2..f308635 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -17,6 +17,8 @@ pub struct HttpClient { headers: reqwest::header::HeaderMap, } +pub type HttpResult = Result; + impl HttpClient { pub fn new() -> Self { let client = reqwest::Client::new(); diff --git a/src/types/entities/challenge_string.rs b/src/types/entities/challenge_string.rs index 3fada3f..a789297 100644 --- a/src/types/entities/challenge_string.rs +++ b/src/types/entities/challenge_string.rs @@ -4,8 +4,9 @@ use std::ops::{Deref, DerefMut}; -use der::asn1::Ia5String; use der::Length; +use ser_der::asn1::Ia5String; +use serde::{Deserialize, Serialize}; use crate::errors::base::ConstraintError; use crate::errors::composite::ConversionError; @@ -13,7 +14,7 @@ use crate::key::PrivateKey; use crate::signature::Signature; use crate::Constrained; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] /// A challenge string, used to prove that an actor possesses a private key, without revealing it. pub struct Challenge { pub(crate) challenge: Ia5String, @@ -28,8 +29,9 @@ impl Challenge { /// - **challenge**: The challenge string. /// - **expires**: The UNIX timestamp when the challenge expires. pub fn new(challenge: &str, expires: u64) -> Result { + let ia5string = der::asn1::Ia5String::new(challenge)?; Ok(Self { - challenge: Ia5String::new(challenge)?, + challenge: ia5string.into(), expires, }) } From 0a79c8a9c9a976dbf0cf2888e1c81b7da75f3436 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 5 May 2024 20:18:09 +0200 Subject: [PATCH 029/215] Add Rotate Server ID-Cert route --- src/api/identity/mod.rs | 26 ++++++++++++++++++++++++++ src/errors/composite.rs | 2 ++ 2 files changed, 28 insertions(+) diff --git a/src/api/identity/mod.rs b/src/api/identity/mod.rs index d8b6ae0..2dcef4e 100644 --- a/src/api/identity/mod.rs +++ b/src/api/identity/mod.rs @@ -4,11 +4,17 @@ use reqwest::Method; +use crate::certs::idcert::IdCert; +use crate::key::PublicKey; +use crate::signature::Signature; use crate::types::entities::Challenge; use super::{HttpClient, HttpResult}; impl HttpClient { + /// Request a challenge string. + /// + /// **GET** *{base_url}*/.p2/core/v1/challenge pub async fn get_challenge(&self, base_url: &str) -> HttpResult { let response = self .request( @@ -20,4 +26,24 @@ impl HttpClient { Self::handle_response(response).await } + + /// Rotate the server's identity key. Returns the new server [IdCert]. + /// + /// **PUT** *{base_url}*/.p2/core/v1/key/server + /// - Requires authentication + /// - Requires server administrator privileges + pub async fn rotate_server_identity_key>( + &self, + base_url: &str, + ) -> HttpResult> { + let response = self + .request( + Method::PUT, + &format!("{}/.p2/core/v1/key/server", base_url), + None, + ) + .await?; + + Ok(IdCert::from_pem(response.text().await?.as_str())?) + } } diff --git a/src/errors/composite.rs b/src/errors/composite.rs index 52527b2..c8e702e 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -51,6 +51,8 @@ pub enum RequestError { HttpError(#[from] reqwest::Error), #[error("Failed to deserialize response into expected type")] DeserializationError(#[from] serde_json::Error), + #[error("Failed to convert response into expected type")] + ConversionError(#[from] ConversionError), } impl From for ConversionError { From 06863456b4d5fd1b6f37d215743affc2e11df169 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 6 May 2024 12:46:31 +0200 Subject: [PATCH 030/215] Add supplementary info about what "PublicKeyInfo" is --- src/certs/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 887fc16..141839f 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -76,7 +76,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. From 8a4c2a12ff85acc81d05eeb7d78f151fc5eedfff Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 15 May 2024 11:15:42 +0200 Subject: [PATCH 031/215] 0.9.0-alpha.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f1cc77c..2369f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.8.0" +version = "0.9.0-alpha.2" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From fef0f57fafe118fe6d123944d8ab0caed6cb1155 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 15 May 2024 11:42:31 +0200 Subject: [PATCH 032/215] 0.9.0-alpha.3: Add validate_home_server and validate_actor for IdCsr, IdCertTbs, IdCert --- Cargo.toml | 2 +- src/certs/idcert.rs | 19 +++++++++++++++++++ src/certs/idcerttbs.rs | 31 ++++++++++++++++++++++++++++++- src/certs/idcsr.rs | 9 +++++---- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2369f98..739cfc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.2" +version = "0.9.0-alpha.3" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 818e7d0..a853ecf 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -132,6 +132,25 @@ impl> IdCert { pub fn to_pem(self, line_ending: LineEnding) -> Result { Ok(Certificate::try_from(self)?.to_pem(line_ending)?) } + + /// Validates the well-formedness of the [IdCert] and its contents. Fails, if the [Name] or + /// [Capabilities] do not meet polyproto validation criteria for home server certs, or if + /// the signature fails to be verified. + // PRETTYFYME: validate_home_server and validate_actor could be made into a trait? + pub fn validate_home_server(&self) -> Result<(), ConversionError> { + self.validate()?; + self.id_cert_tbs.validate_home_server()?; + Ok(()) + } + + /// Validates the well-formedness of the [IdCert] and its contents. Fails, if the [Name] or + /// [Capabilities] do not meet polyproto validation criteria for actor certs, or if + /// the signature fails to be verified. + pub fn validate_actor(&self) -> Result<(), ConversionError> { + self.validate()?; + self.id_cert_tbs.validate_actor()?; + Ok(()) + } } impl> TryFrom> for Certificate { diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 5814761..49b966f 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -12,7 +12,7 @@ use x509_cert::serial_number::SerialNumber; use x509_cert::time::Validity; use x509_cert::TbsCertificate; -use crate::errors::base::InvalidInput; +use crate::errors::base::{ConstraintError, InvalidInput}; use crate::errors::composite::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; @@ -145,6 +145,35 @@ impl> IdCertTbs { pub fn from_der(bytes: &[u8]) -> Result { IdCertTbs::try_from(TbsCertificate::from_der(bytes)?) } + + /// Validates the well-formedness of the [IdCertTbs] and its contents. Fails, if the [Name] or + /// [Capabilities] do not meet polyproto validation criteria for home server certs, or if + /// the signature fails to be verified. + // PRETTYFYME: validate_home_server and validate_actor could be made into a trait? + pub fn validate_home_server(&self) -> Result<(), ConversionError> { + self.validate()?; + if !self.capabilities.basic_constraints.ca { + return Err(ConversionError::ConstraintError( + ConstraintError::Malformed(Some( + "Home server cert must have the CA capability set to true".to_string(), + )), + )); + } + Ok(()) + } + + /// Validates the well-formedness of the [IdCertTbs] and its contents. Fails, if the [Name] or + /// [Capabilities] do not meet polyproto validation criteria for actor certs, or if + /// the signature fails to be verified. + pub fn validate_actor(&self) -> Result<(), ConversionError> { + self.validate()?; + if self.capabilities.basic_constraints.ca { + return Err(ConversionError::ConstraintError( + ConstraintError::Malformed(Some("Actor cert must not be a CA".to_string())), + )); + } + Ok(()) + } } impl> TryFrom> diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 04297c5..d2927e4 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -79,8 +79,7 @@ impl> IdCsr { /// 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> { + pub fn validate_actor(&self) -> Result<(), ConversionError> { self.validate()?; if self.inner_csr.capabilities.basic_constraints.ca { return Err(ConversionError::ConstraintError( @@ -93,11 +92,13 @@ impl> IdCsr { /// 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> { + pub fn validate_home_server(&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())), + ConstraintError::Malformed(Some( + "Home server CSR must have the CA capability set to true".to_string(), + )), )); } Ok(()) From 390ecd62a7410181dc686ef644fe7fe10e03c96d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 15 May 2024 11:52:12 +0200 Subject: [PATCH 033/215] 0.9.0-alpha.3: Normalize from_der() on all Csr, CertTbs, Cert --- src/certs/idcert.rs | 4 ++-- src/certs/idcsr.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index a853ecf..82487bb 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -110,8 +110,8 @@ impl> IdCert { } /// 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)?)?; + pub fn from_der(value: &[u8]) -> Result { + let cert = IdCert::try_from(Certificate::from_der(value)?)?; cert.validate()?; Ok(cert) } diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index d2927e4..a620656 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -105,6 +105,7 @@ impl> IdCsr { } /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. + // PRETTYFYME: Could be a trait along with to_der, from_pem, to_pem pub fn from_der(bytes: &[u8]) -> Result { IdCsr::try_from(CertReq::from_der(bytes)?) } From 44d9c2810a74ea5e222e109ac54a04033eccb5d3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 15 May 2024 13:18:30 +0200 Subject: [PATCH 034/215] Rename Signature::from_bitstring to ::from_bytes, since the name is more fitting --- examples/ed25519_basic.rs | 2 +- examples/ed25519_cert.rs | 2 +- examples/ed25519_from_der.rs | 4 ++-- src/certs/idcert.rs | 2 +- src/certs/idcsr.rs | 2 +- src/signature.rs | 4 ++-- tests/idcert.rs | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/ed25519_basic.rs b/examples/ed25519_basic.rs index 8684489..a642e0a 100644 --- a/examples/ed25519_basic.rs +++ b/examples/ed25519_basic.rs @@ -102,7 +102,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] = { diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index 8e570ad..f251d2b 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -127,7 +127,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] = { diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index b228ae2..d172041 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -74,7 +74,7 @@ fn main() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); - let cert_from_der = IdCert::from_der(data).unwrap(); + let cert_from_der = IdCert::from_der(&data).unwrap(); assert_eq!(cert_from_der, cert) } @@ -119,7 +119,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] = { diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 82487bb..41a59c0 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -169,7 +169,7 @@ impl> TryFrom for IdCert { fn try_from(value: Certificate) -> Result { let id_cert_tbs = value.tbs_certificate.try_into()?; - let signature = S::from_bitstring(value.signature.raw_bytes()); + let signature = S::from_bytes(value.signature.raw_bytes()); Ok(IdCert { id_cert_tbs, signature, diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index a620656..6ad8e9c 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -191,7 +191,7 @@ impl> TryFrom for IdCsr { 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()), }) } } diff --git a/src/signature.rs b/src/signature.rs index 492fa23..0599a8a 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -11,6 +11,6 @@ pub trait Signature: PartialEq + Eq + SignatureBitStringEncoding + Clone + ToStr 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/tests/idcert.rs b/tests/idcert.rs index c4a9377..222a523 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -183,7 +183,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] = { From bcd5cec8853082e9d949bf3092fc7e25550b9d58 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 15 May 2024 14:57:55 +0200 Subject: [PATCH 035/215] remove polyproto dev depenency --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 739cfc7..f4445e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ ser_der = { version = "0.1.0-alpha.1", optional = true, features = ["alloc"] } [dev-dependencies] ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } rand = "0.8.5" -polyproto = { path = "./" } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.42" From ed365531f1a3b414e85f0a935fcfdfc755ad4db7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 15 May 2024 15:08:33 +0200 Subject: [PATCH 036/215] Fix build fail when building without default features --- src/errors/composite.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/errors/composite.rs b/src/errors/composite.rs index c8e702e..edded0d 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -44,7 +44,7 @@ pub enum ConversionError { #[error(transparent)] IdCertError(#[from] PublicKeyError), } - +#[cfg(feature = "routes")] #[derive(Error, Debug)] pub enum RequestError { #[error(transparent)] From e1d7dfddd1f86397d088024d26ee8e2e6cdbf23c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 15:57:57 +0200 Subject: [PATCH 037/215] 0.9.0-alpha.4: re-export x509_cert::name::*; --- Cargo.toml | 2 +- src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f4445e4..0866a29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.3" +version = "0.9.0-alpha.4" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" diff --git a/src/lib.rs b/src/lib.rs index 6afce36..e2a9110 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,7 @@ pub(crate) 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. From 2608e1d3b6e4a5fc4264a5a11e2e8d34e88a40e7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 17:00:38 +0200 Subject: [PATCH 038/215] save work --- Cargo.toml | 4 +- examples/ed25519_cert.rs | 4 + src/certs/idcsr.rs | 12 ++- tests/blubb.rs | 217 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 tests/blubb.rs diff --git a/Cargo.toml b/Cargo.toml index 0866a29..ad89359 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ reqwest = ["dep:reqwest", "routes"] serde = ["dep:serde", "dep:serde_json", "dep:ser_der"] [dependencies] -der = "0.7.9" +der = { version = "0.7.9", features = ["pem"] } getrandom = { version = "0.2.14", optional = true } regex = "1.10.4" reqwest = { version = "0.12.4", features = ["json"], optional = true } @@ -27,7 +27,7 @@ serde = { version = "1.0.199", optional = true, features = ["derive"] } serde_json = { version = "1.0.116", optional = true } spki = "0.7.3" thiserror = "1.0.59" -x509-cert = { version = "0.2.5", default-features = false, features = ["pem"] } +x509-cert = "0.2.5" ser_der = { version = "0.1.0-alpha.1", optional = true, features = ["alloc"] } [dev-dependencies] diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index f251d2b..0aa5009 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -56,6 +56,10 @@ fn main() { &Capabilities::default_actor(), ) .unwrap(); + let csr_pem = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let csr_from_pem = + polyproto::certs::idcsr::IdCsr::::from_pem(&csr_pem) + .unwrap(); let data = csr.clone().to_der().unwrap(); let file_name_with_extension = "cert.csr"; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 6ad8e9c..ef17b27 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -188,6 +188,10 @@ impl> TryFrom for IdCsr { type Error = ConversionError; fn try_from(value: CertReq) -> Result { + dbg!("====="); + dbg!(&value.info.subject.to_string()); + dbg!(IdCsrInner::::try_from(value.info.clone()).is_err()); + dbg!("====="); Ok(IdCsr { inner_csr: IdCsrInner::try_from(value.info)?, signature_algorithm: value.algorithm, @@ -206,11 +210,15 @@ impl> TryFrom for IdCsrInner { algorithm: value.public_key.algorithm, public_key_bitstring: value.public_key.subject_public_key, }; - + let public_key = P::try_from_public_key_info(public_key_info); + if public_key.is_err() { + eprintln!("{}", public_key.clone().err().unwrap()); + } + let public_key = public_key?; Ok(IdCsrInner { version: PkcsVersion::V1, subject: rdn_sequence, - subject_public_key: PublicKey::try_from_public_key_info(public_key_info)?, + subject_public_key: public_key, capabilities: Capabilities::try_from(value.attributes)?, phantom_data: PhantomData, }) diff --git a/tests/blubb.rs b/tests/blubb.rs new file mode 100644 index 0000000..e78d328 --- /dev/null +++ b/tests/blubb.rs @@ -0,0 +1,217 @@ +// 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 der::asn1::{BitString, Ia5String, 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::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 +/// 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 +/// ``` + +#[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()); + 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_actor(), + ) + .unwrap(); + let csr_pem = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let csr_from_pem = + polyproto::certs::idcsr::IdCsr::::from_pem(&csr_pem) + .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, +} + +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. + type Signature = Ed25519DalekSignature; + + // This is straightforward: we return a reference to the signature. + fn as_signature(&self) -> &Self::Signature { + &self.signature + } + + // The algorithm identifier for a given signature implementation is constant. We just need + // to define it here. + fn algorithm_identifier() -> AlgorithmIdentifierOwned { + AlgorithmIdentifierOwned { + // This is the OID for Ed25519. It is defined in the IANA registry. + oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), + // For this example, we don't need or want any parameters. + parameters: None, + } + } + + #[cfg(not(tarpaulin_include))] + fn from_bytes(signature: &[u8]) -> Self { + let mut signature_vec = signature.to_vec(); + signature_vec.resize(64, 0); + let signature_array: [u8; 64] = { + let mut array = [0; 64]; + array.copy_from_slice(&signature_vec[..]); + array + }; + Self { + signature: Ed25519DalekSignature::from_bytes(&signature_array), + algorithm: Self::algorithm_identifier(), + } + } +} + +// The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement +// it for our signature type. +impl SignatureBitStringEncoding for Ed25519Signature { + fn to_bitstring(&self) -> der::Result { + BitString::from_bytes(&self.as_signature().to_bytes()) + } +} + +// Next, we implement the key traits. We start by defining the private key type. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Ed25519PrivateKey { + // Defined below + public_key: Ed25519PublicKey, + // The private key from the ed25519-dalek crate + key: SigningKey, +} + +impl PrivateKey for Ed25519PrivateKey { + type PublicKey = Ed25519PublicKey; + + // Return a reference to the public key + fn pubkey(&self) -> &Self::PublicKey { + &self.public_key + } + + // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can + // harness all of its functionality, such as the `sign` method. + fn sign(&self, data: &[u8]) -> Ed25519Signature { + let signature = self.key.sign(data); + Ed25519Signature { + signature, + algorithm: self.algorithm_identifier(), + } + } +} + +impl Ed25519PrivateKey { + // Let's also define a handy method to generate a key pair. + pub fn gen_keypair(csprng: &mut OsRng) -> Self { + let key = SigningKey::generate(csprng); + let public_key = Ed25519PublicKey { + key: key.verifying_key(), + }; + Self { public_key, key } + } +} + +// Same thing as above for the public key type. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Ed25519PublicKey { + // The public key type from the ed25519-dalek crate + key: VerifyingKey, +} + +impl PublicKey for Ed25519PublicKey { + // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. + // This method is used to mitigate weak key forgery. + #[cfg(not(tarpaulin_include))] + fn verify_signature( + &self, + signature: &Ed25519Signature, + data: &[u8], + ) -> Result<(), polyproto::errors::composite::PublicKeyError> { + match self.key.verify_strict(data, signature.as_signature()) { + Ok(_) => Ok(()), + Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), + } + } + + // Returns the public key info. Public key info is used to encode the public key in a + // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 + // standard, and thus includes the information needed to encode the public key in a certificate + // or a CSR. + fn public_key_info(&self) -> PublicKeyInfo { + PublicKeyInfo { + algorithm: Ed25519Signature::algorithm_identifier(), + public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), + } + } + + #[cfg(not(tarpaulin_include))] + fn try_from_public_key_info( + public_key_info: PublicKeyInfo, + ) -> std::result::Result { + let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); + key_vec.resize(32, 0); + let signature_array: [u8; 32] = { + let mut array = [0; 32]; + array.copy_from_slice(&key_vec[..]); + array + }; + Ok(Self { + key: VerifyingKey::from_bytes(&signature_array).unwrap(), + }) + } +} From 7b0f5595e597ea56460ad12656476468306d1f30 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 17:43:02 +0200 Subject: [PATCH 039/215] =?UTF-8?q?bug=20found=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/certs/capabilities/key_usage.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 1b6fec2..1eafb39 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -202,6 +202,8 @@ impl TryFrom for KeyUsages { type Error = ConversionError; fn try_from(value: Attribute) -> Result { + // The issue seems to be that the value is a SetOfVec CONTAINING a BitString, rather than + // just a BitString itself. if value.tag() != Tag::BitString { return Err(ConversionError::InvalidInput(InvalidInput::Malformed( format!("Expected BitString, found {}", value.tag(),), From 064b0e0c2aab9a0ebd6fb0669301e355d3ff80f9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 17:44:10 +0200 Subject: [PATCH 040/215] Add test: ID-CSR to and then from PEM --- tests/{blubb.rs => idcsr.rs} | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) rename tests/{blubb.rs => idcsr.rs} (92%) diff --git a/tests/blubb.rs b/tests/idcsr.rs similarity index 92% rename from tests/blubb.rs rename to tests/idcsr.rs index e78d328..3e7f291 100644 --- a/tests/blubb.rs +++ b/tests/idcsr.rs @@ -47,7 +47,7 @@ use x509_cert::Certificate; /// ``` #[test] -fn main() { +fn id_csr_to_from_pem() { let mut csprng = rand::rngs::OsRng; let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); println!("Private Key is: {:?}", priv_key.key.to_bytes()); @@ -123,7 +123,10 @@ impl Signature for Ed25519Signature { // it for our signature type. impl SignatureBitStringEncoding for Ed25519Signature { fn to_bitstring(&self) -> der::Result { - BitString::from_bytes(&self.as_signature().to_bytes()) + let unused_bits: u8 = (self.as_signature().to_bytes().len() % 8) + .try_into() + .unwrap(); + BitString::new(unused_bits, self.as_signature().to_bytes().to_vec()) } } @@ -210,8 +213,14 @@ impl PublicKey for Ed25519PublicKey { array.copy_from_slice(&key_vec[..]); array }; - Ok(Self { - key: VerifyingKey::from_bytes(&signature_array).unwrap(), - }) + match VerifyingKey::from_bytes(&signature_array) { + Ok(key) => Ok(Ed25519PublicKey { key }), + Err(e) => Err(polyproto::errors::composite::ConversionError::InvalidInput( + polyproto::errors::base::InvalidInput::Malformed(format!( + "Could not convert public key: {}", + e + )), + )), + } } } From d14d9701ec43f296f62971b220abb4607a9bfa34 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 17:44:16 +0200 Subject: [PATCH 041/215] remove debugging crap --- src/certs/idcsr.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index ef17b27..0e9cfde 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -188,10 +188,6 @@ impl> TryFrom for IdCsr { type Error = ConversionError; fn try_from(value: CertReq) -> Result { - dbg!("====="); - dbg!(&value.info.subject.to_string()); - dbg!(IdCsrInner::::try_from(value.info.clone()).is_err()); - dbg!("====="); Ok(IdCsr { inner_csr: IdCsrInner::try_from(value.info)?, signature_algorithm: value.algorithm, @@ -210,15 +206,11 @@ impl> TryFrom for IdCsrInner { algorithm: value.public_key.algorithm, public_key_bitstring: value.public_key.subject_public_key, }; - let public_key = P::try_from_public_key_info(public_key_info); - if public_key.is_err() { - eprintln!("{}", public_key.clone().err().unwrap()); - } - let public_key = public_key?; + dbg!(Capabilities::try_from(value.attributes.clone()).is_err()); Ok(IdCsrInner { version: PkcsVersion::V1, subject: rdn_sequence, - subject_public_key: public_key, + subject_public_key: PublicKey::try_from_public_key_info(public_key_info)?, capabilities: Capabilities::try_from(value.attributes)?, phantom_data: PhantomData, }) From 3c41d0fc61bda30a9d09090086b5101519d1d97a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 18:14:00 +0200 Subject: [PATCH 042/215] progress --- examples/ed25519_cert.rs | 4 ---- src/certs/capabilities/key_usage.rs | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index 0aa5009..f251d2b 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -56,10 +56,6 @@ fn main() { &Capabilities::default_actor(), ) .unwrap(); - let csr_pem = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let csr_from_pem = - polyproto::certs::idcsr::IdCsr::::from_pem(&csr_pem) - .unwrap(); let data = csr.clone().to_der().unwrap(); let file_name_with_extension = "cert.csr"; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 1eafb39..0875ceb 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -204,9 +204,9 @@ impl TryFrom for KeyUsages { fn try_from(value: Attribute) -> Result { // The issue seems to be that the value is a SetOfVec CONTAINING a BitString, rather than // just a BitString itself. - if value.tag() != Tag::BitString { + if value.tag() != Tag::BitString && value.tag() != Tag::Sequence { return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - format!("Expected BitString, found {}", value.tag(),), + format!("Expected BitString or Sequence, found {}", value.tag(),), ))); } match value.values.len() { @@ -220,7 +220,9 @@ impl TryFrom for KeyUsages { })); } }; + dbg!(&value); let inner_value = value.values.get(0).expect("Illegal state. Please report this error to https://github.com/polyphony-chat/polyproto"); + dbg!(inner_value.value()); KeyUsages::from_bitstring(BitString::from_der(inner_value.value())?) } } From 3f26f7f77bc4d117aee6e51567fcf382ad368a64 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 21:36:35 +0200 Subject: [PATCH 043/215] further problem analysis --- src/certs/capabilities/key_usage.rs | 40 ++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 0875ceb..5e34e9a 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -202,11 +202,41 @@ impl TryFrom for KeyUsages { type Error = ConversionError; fn try_from(value: Attribute) -> Result { - // The issue seems to be that the value is a SetOfVec CONTAINING a BitString, rather than - // just a BitString itself. - if value.tag() != Tag::BitString && value.tag() != Tag::Sequence { + // The issue seems to be that the BitString is invalid. + /* + Good BitString: + Any { + tag: Tag(0x03: BIT STRING), + value: BytesOwned { + length: Length( + 4, + ), + inner: [ + 3, + 2, + 0, + 255, + ], + }, + } + + Bad BitString: + Any { + tag: Tag(0x03: BIT STRING), + value: BytesOwned { + length: Length( + 2, + ), + inner: [ + 0, <- Missing Tag "3", Missing Length "2" + 128, + ], + }, + } + */ + if value.tag() != Tag::Sequence { return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - format!("Expected BitString or Sequence, found {}", value.tag(),), + format!("Expected Sequence, found {}", value.tag(),), ))); } match value.values.len() { @@ -220,9 +250,7 @@ impl TryFrom for KeyUsages { })); } }; - dbg!(&value); let inner_value = value.values.get(0).expect("Illegal state. Please report this error to https://github.com/polyphony-chat/polyproto"); - dbg!(inner_value.value()); KeyUsages::from_bitstring(BitString::from_der(inner_value.value())?) } } From 1c5d4c2f19f892d59fd0e22709b472660c19250a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 22:20:21 +0200 Subject: [PATCH 044/215] Fix IdCsrs unable to be built from PEM This (removed) block of code was based on the assumption that `bitstring.raw_bytes().to_vec()` resulted in a value looking like: `[3, 2, 0, 128]`, which is the der::Any encoding for a BitString. However, the value looks like this instead: `[128]` --- src/certs/capabilities/key_usage.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 5e34e9a..7de8b25 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -91,12 +91,6 @@ 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); let mut key_usages = Vec::new(); if byte_array.len() == 2 { // If the length of the byte array is 2, this means that DecipherOnly is set. @@ -278,7 +272,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::new(Tag::BitString, bitstring.to_der()?)?; + sov.insert(any)?; Ok(Attribute { oid: ObjectIdentifier::from_str(OID_KEY_USAGE)?, values: sov, From 0bd08c9bb95b03205936961f2e2ef29a0fec21ea Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 22:21:25 +0200 Subject: [PATCH 045/215] Remove dbg!() --- src/certs/idcsr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 0e9cfde..1a9b0bd 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -206,7 +206,6 @@ impl> TryFrom for IdCsrInner { algorithm: value.public_key.algorithm, public_key_bitstring: value.public_key.subject_public_key, }; - dbg!(Capabilities::try_from(value.attributes.clone()).is_err()); Ok(IdCsrInner { version: PkcsVersion::V1, subject: rdn_sequence, From 12c0ecb0637c56461d076b0c2d028218894f163c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 22:22:09 +0200 Subject: [PATCH 046/215] remove import --- tests/idcsr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 3e7f291..280e571 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -13,7 +13,6 @@ 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::Capabilities; use polyproto::certs::idcert::IdCert; From f25dbc9d4a9d7b90694f9c5cea78da2ae2c27b29 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 23:01:18 +0200 Subject: [PATCH 047/215] 0.9.0-alpha.5: Fix regression/oversight in alpha.4 causing test failure Add test cert_from_der() --- Cargo.toml | 2 +- src/certs/capabilities/key_usage.rs | 11 ++++++++ tests/idcert.rs | 44 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ad89359..947835e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.4" +version = "0.9.0-alpha.5" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 7de8b25..a4f9330 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -91,7 +91,17 @@ impl KeyUsages { /// ``` pub fn from_bitstring(bitstring: BitString) -> Result { let mut byte_array = bitstring.raw_bytes().to_vec(); + dbg!(&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 }); + } + // TODO: Instead of doing this, we should rather find out why the first byte is sometimes 0. + // works for now though. -bitfl0wer + 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); @@ -128,6 +138,7 @@ impl KeyUsages { "Could not properly convert this BitString to KeyUsages. The BitString is malformed".to_string(), ))); } + dbg!(&key_usages); Ok(KeyUsages { key_usages }) } diff --git a/tests/idcert.rs b/tests/idcert.rs index 222a523..edeac80 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -146,6 +146,50 @@ fn test_create_invalid_actor_csr() { assert!(csr.is_err()); } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cert_from_der() { + 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=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), + &priv_key, + &Capabilities::default_actor(), + ) + .unwrap(); + + let mut 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(), + 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(); + dbg!(&cert.id_cert_tbs.capabilities.key_usage); + dbg!(&cert + .id_cert_tbs + .capabilities + .key_usage + .clone() + .to_bitstring() + .raw_bytes()); + let data = cert.clone().to_der().unwrap(); + let cert_from_der = IdCert::from_der(&data).unwrap(); + assert_eq!(cert_from_der, cert) +} + // 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 From e334064fd0362095ac1c42443f67691180f6cfef Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 23:01:38 +0200 Subject: [PATCH 048/215] change actions workflow to run on ALL prs --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 22fdd5c..cc987b0 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 From 236127b528a995dd5b3acd9e156d5c802dbb2bcc Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 16 May 2024 23:06:26 +0200 Subject: [PATCH 049/215] remove dbg!() --- src/certs/capabilities/key_usage.rs | 2 -- tests/idcert.rs | 8 -------- 2 files changed, 10 deletions(-) diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index a4f9330..5f8dca1 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -91,7 +91,6 @@ impl KeyUsages { /// ``` pub fn from_bitstring(bitstring: BitString) -> Result { let mut byte_array = bitstring.raw_bytes().to_vec(); - dbg!(&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 @@ -138,7 +137,6 @@ impl KeyUsages { "Could not properly convert this BitString to KeyUsages. The BitString is malformed".to_string(), ))); } - dbg!(&key_usages); Ok(KeyUsages { key_usages }) } diff --git a/tests/idcert.rs b/tests/idcert.rs index edeac80..35655cb 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -177,14 +177,6 @@ fn cert_from_der() { }, ) .unwrap(); - dbg!(&cert.id_cert_tbs.capabilities.key_usage); - dbg!(&cert - .id_cert_tbs - .capabilities - .key_usage - .clone() - .to_bitstring() - .raw_bytes()); let data = cert.clone().to_der().unwrap(); let cert_from_der = IdCert::from_der(&data).unwrap(); assert_eq!(cert_from_der, cert) From 6ccc4b1f08fb8e5b74e6804508e589530e8b8c63 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 13:23:24 +0200 Subject: [PATCH 050/215] add csr_from_der, cert_from_pem tests --- tests/idcert.rs | 6 +++--- tests/idcsr.rs | 33 ++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/tests/idcert.rs b/tests/idcert.rs index 35655cb..1adfe2c 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -148,7 +148,7 @@ fn test_create_invalid_actor_csr() { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] -fn cert_from_der() { +fn cert_from_pem() { let mut csprng = rand::rngs::OsRng; let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); @@ -177,8 +177,8 @@ fn cert_from_der() { }, ) .unwrap(); - let data = cert.clone().to_der().unwrap(); - let cert_from_der = IdCert::from_der(&data).unwrap(); + let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let cert_from_der = IdCert::from_pem(&data).unwrap(); assert_eq!(cert_from_der, cert) } diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 280e571..fbb342b 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -16,6 +16,7 @@ use der::asn1::{BitString, Ia5String, Uint, UtcTime}; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; +use polyproto::certs::idcsr::IdCsr; use polyproto::certs::PublicKeyInfo; use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; @@ -45,13 +46,11 @@ use x509_cert::Certificate; /// openssl x509 -in cert.der -text -noout -inform der /// ``` -#[test] -fn id_csr_to_from_pem() { +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn csr_from_pem() { 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(), @@ -59,10 +58,26 @@ fn id_csr_to_from_pem() { &Capabilities::default_actor(), ) .unwrap(); - let csr_pem = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let csr_from_pem = - polyproto::certs::idcsr::IdCsr::::from_pem(&csr_pem) - .unwrap(); + let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let csr_from_der = IdCsr::from_pem(&data).unwrap(); + assert_eq!(csr_from_der, csr) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn csr_from_der() { + 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=www,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1").unwrap(), + &priv_key, + &Capabilities::default_actor(), + ) + .unwrap(); + let data = csr.clone().to_der().unwrap(); + let csr_from_der = IdCsr::from_der(&data).unwrap(); + assert_eq!(csr_from_der, csr) } // As mentioned in the README, we start by implementing the signature trait. From 6123dee905f874fa6a7f52278b806fb8733dc068 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 15:04:25 +0200 Subject: [PATCH 051/215] Add pub fn signature_data() --- src/certs/idcert.rs | 10 ++++++++++ src/certs/idcsr.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 41a59c0..771df48 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -151,6 +151,16 @@ impl> IdCert { self.id_cert_tbs.validate_actor()?; Ok(()) } + + /// 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() + } } impl> TryFrom> for Certificate { diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 1a9b0bd..abad922 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -124,6 +124,16 @@ impl> IdCsr { 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`: From ba2838d6c91956215bd6fec9b1fbf05a3fa3e6b5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 18:11:28 +0200 Subject: [PATCH 052/215] add logging dependencies --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 947835e..2617897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,9 +29,11 @@ spki = "0.7.3" thiserror = "1.0.59" x509-cert = "0.2.5" ser_der = { version = "0.1.0-alpha.1", optional = true, features = ["alloc"] } +log = "0.4.21" [dev-dependencies] ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } +env_logger = "0.11.3" rand = "0.8.5" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] From 2f9138cb9ad4f8c91ae492cf396fbfece36a578f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 18:12:38 +0200 Subject: [PATCH 053/215] 0.9.0-alpha.6: Check if DCs of UID and DC field(s) match --- src/constraints.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++-- tests/idcert.rs | 67 +++++++++++++++++++++++++++++---- tests/idcsr.rs | 10 ++++- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/src/constraints.rs b/src/constraints.rs index 4b79d42..6c446e8 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -2,10 +2,13 @@ // 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 std::vec; + use der::asn1::Ia5String; use der::Length; +use log::debug; use regex::Regex; -use x509_cert::name::Name; +use x509_cert::name::{Name, RelativeDistinguishedName}; use crate::certs::capabilities::{Capabilities, KeyUsage}; use crate::certs::idcert::IdCert; @@ -33,10 +36,13 @@ impl Constrained for Name { /// - 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> { + // PRETTYFYME(bitfl0wer): This function is too long. Refactor it. 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 uid: RelativeDistinguishedName = RelativeDistinguishedName::default(); + let mut vec_dc: Vec = Vec::new(); let rdns = &self.0; for rdn in rdns.iter() { @@ -44,6 +50,7 @@ impl Constrained for Name { match item.oid.to_string().as_str() { OID_RDN_UID => { num_uid += 1; + uid = rdn.clone(); let fid_regex = Regex::new(r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)") .expect("Regex failed to compile"); @@ -78,7 +85,10 @@ impl Constrained for Name { }); } } - OID_RDN_DOMAIN_COMPONENT => num_dc += 1, + OID_RDN_DOMAIN_COMPONENT => { + num_dc += 1; + vec_dc.push(rdn.clone()); + } _ => {} } } @@ -124,6 +134,52 @@ impl Constrained for Name { reason: "Actors must have uniqueIdentifier AND UID, only UID found".to_string(), }); } + + // Only check if we are dealing with an actor + if num_uid > 0 && num_unique_identifier > 0 { + vec_dc.reverse(); // The order of the DCs is reversed in the [Name] object, starting with the TLD + + // Check if the domain components are equal between the UID and the DCs + // First, remove the "username@" from the UID + // We can unwrap, because an @ is guaranteed to be included in the string. The regex above + // makes sure of that. + let position_of_at = uid.to_string().find('@').unwrap(); + 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; + for component in dc_normalized_uid.iter() { + debug!("Checking if component \"{}\"...", component); + let equivalent_dc = match vec_dc.get(index as usize) { + Some(dc) => dc, + None => { + return Err(ConstraintError::Malformed(Some( + "Domain Components do not equal the domain components in the UID" + .to_string(), + ))) + } + }; + let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); + debug!( + "...is equal to component \"{}\"...", + equivalent_dc.to_string() + ); + if component != &equivalent_dc.to_string() { + return Err(ConstraintError::Malformed(Some( + "Domain Components do not equal the domain components in the UID" + .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(()) } } @@ -284,11 +340,13 @@ mod name_constraints { use x509_cert::name::Name; + 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", ) @@ -296,11 +354,34 @@ mod name_constraints { name.validate().unwrap(); let name = Name::from_str("CN=flori,DC=www,DC=polyphony,DC=chat").unwrap(); name.validate().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().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 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().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().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 name = Name::from_str("CN=flori").unwrap(); assert!(name.validate().is_err()); } @@ -308,6 +389,7 @@ mod name_constraints { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] fn two_cns() { + init_logger(); let name = Name::from_str("CN=flori,CN=xenia,DC=localhost").unwrap(); assert!(name.validate().is_err()) } @@ -315,6 +397,7 @@ mod name_constraints { #[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 name = Name::from_str("CN=flori,CN=xenia,uid=numbaone,uid=numbatwo").unwrap(); assert!(name.validate().is_err()); let name = @@ -326,6 +409,7 @@ mod name_constraints { #[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 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(); @@ -334,6 +418,7 @@ mod name_constraints { #[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 name = Name::from_str("cn=flori,dc=localhost,uid=flori@localhost,uniqueIdentifier=").unwrap(); assert!(name.validate().is_err()); @@ -345,11 +430,12 @@ mod name_constraints { #[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 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") + 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(); diff --git a/tests/idcert.rs b/tests/idcert.rs index 1adfe2c..5289b4e 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -56,7 +56,10 @@ fn test_create_actor_cert() { .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(), + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), &priv_key, &capabilities, ) @@ -66,7 +69,7 @@ fn test_create_actor_cert() { &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", + "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", ) .unwrap(), Validity { @@ -94,7 +97,10 @@ fn test_create_ca_cert() { 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_home_server(), ) @@ -104,7 +110,7 @@ fn test_create_ca_cert() { &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", + "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", ) .unwrap(), Validity { @@ -139,13 +145,57 @@ fn test_create_invalid_actor_csr() { .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(), + &RdnSequence::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(), &priv_key, &capabilities, ); assert!(csr.is_err()); } +#[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() { + 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=example,DC=com,UID=flori@example.com,uniqueIdentifier=client1", + ) + .unwrap(), + &priv_key, + &Capabilities::default_home_server(), + ) + .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,UID=root@polyphony.chat,uniqueIdentifier=root", + ) + .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() { @@ -153,7 +203,10 @@ fn cert_from_pem() { let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 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(), ) @@ -164,7 +217,7 @@ fn cert_from_pem() { &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", + "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", ) .unwrap(), Validity { diff --git a/tests/idcsr.rs b/tests/idcsr.rs index fbb342b..1413124 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -53,7 +53,10 @@ fn csr_from_pem() { let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 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(), ) @@ -70,7 +73,10 @@ fn csr_from_der() { let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 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(), ) From 9b82831c8276e88c97f784fd1588ae9eea89eacd Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 18:12:49 +0200 Subject: [PATCH 054/215] Add init_logger method for testing --- src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index e2a9110..9e62698 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,17 @@ pub trait Constrained { fn validate(&self) -> Result<(), ConstraintError>; } +#[cfg(test)] +pub(crate) mod testing_utils { + pub(crate) fn init_logger() { + 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; From cf9ec41ee62be475809a3ff289578acec08aff08 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 18:13:15 +0200 Subject: [PATCH 055/215] Add TODO --- src/certs/idcsr.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index abad922..d9c0db0 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -94,6 +94,8 @@ impl> IdCsr { /// the signature fails to be verified. pub fn validate_home_server(&self) -> Result<(), ConversionError> { self.validate()?; + // TODO: There are missing constraints here. The home server CSR must not have a UID/uniqueIdentifier + // field set, for example. if !self.inner_csr.capabilities.basic_constraints.ca { return Err(ConversionError::ConstraintError( ConstraintError::Malformed(Some( From ea381baed48c718ed6e66d29ff86837f85c9e38d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 18:47:23 +0200 Subject: [PATCH 056/215] Add ActorConstrained, HomeServerConstrained traits --- src/lib.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9e62698..24397b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,8 +73,13 @@ 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. In addition to the [Constrained] +/// trait, types can also implement [ActorConstrained] and [HomeServerConstrained] to add additional +/// guarantees about their well-formedness in the context of an actor or home server. If implemented, +/// these trait methods should be preferred over the [Constrained] trait method. +/// +/// [Constrained] does not guarantee that a validated type will always be *correct* in the context +/// it is in. /// /// ### Example /// @@ -85,6 +90,34 @@ pub trait Constrained { fn validate(&self) -> Result<(), ConstraintError>; } +/// Types implementing [ActorConstrained] act just like types implementing [Constrained], with the +/// additional constraint that they must be well-formed in the context of an actor. +/// +/// For example: While [Constrained] might validate that an IdCert is well-formed, [ActorConstrained] +/// would also check that the IdCert is not a CA certificate, among other things. +/// +/// ## Implementing [ActorConstrained] +/// +/// As [ActorConstrained] is supposed to offer a super-set of the guarantees offered by [Constrained], +/// it is recommended to call [Constrained::validate] in the implementation of [ActorConstrained::validate_actor]. +pub trait ActorConstrained: Constrained { + fn validate_actor(&self) -> Result<(), ConstraintError>; +} + +/// Types implementing [HomeServerConstrained] act just like types implementing [Constrained], with the +/// additional constraint that they must be well-formed in the context of an actor. +/// +/// For example: While [Constrained] might validate that an IdCert is well-formed, [HomeServerConstrained] +/// would also check that the IdCert is a CA certificate, among other things. +/// +/// ## Implementing [HomeServerConstrained] +/// +/// As [HomeServerConstrained] is supposed to offer a super-set of the guarantees offered by [Constrained], +/// it is recommended to call [Constrained::validate] in the implementation of [HomeServerConstrained::validate_actor]. +pub trait HomeServerConstrained: Constrained { + fn validate_home_server(&self) -> Result<(), ConstraintError>; +} + #[cfg(test)] pub(crate) mod testing_utils { pub(crate) fn init_logger() { From a07a10c52ca10579d63cb747a05de49065cfbd27 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 18:52:26 +0200 Subject: [PATCH 057/215] remove unused import --- src/constraints.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/constraints.rs b/src/constraints.rs index 6c446e8..e3146cf 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -2,8 +2,6 @@ // 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 std::vec; - use der::asn1::Ia5String; use der::Length; use log::debug; From b2c7a3de8c751102f85bdf4949ead860c07b6d1f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:20:34 +0200 Subject: [PATCH 058/215] Shorten Name::validate(), impl ActorConstrained for Name --- src/constraints.rs | 155 +++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 61 deletions(-) diff --git a/src/constraints.rs b/src/constraints.rs index e3146cf..048dac3 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -17,10 +17,12 @@ 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, + ActorConstrained, Constrained, OID_RDN_COMMON_NAME, OID_RDN_DOMAIN_COMPONENT, OID_RDN_UID, OID_RDN_UNIQUE_IDENTIFIER, }; +use self::rdn::{validate_rdn_uid, validate_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 @@ -34,12 +36,10 @@ impl Constrained for Name { /// - 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> { - // PRETTYFYME(bitfl0wer): This function is too long. Refactor it. 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 uid: RelativeDistinguishedName = RelativeDistinguishedName::default(); let mut vec_dc: Vec = Vec::new(); let rdns = &self.0; @@ -48,29 +48,11 @@ impl Constrained for Name { match item.oid.to_string().as_str() { OID_RDN_UID => { num_uid += 1; - uid = rdn.clone(); - 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(), - ))); - } + validate_rdn_uid(item)?; } 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(), - ))); - } + validate_rdn_unique_identifier(item)?; } OID_RDN_COMMON_NAME => { num_cn += 1; @@ -132,56 +114,107 @@ impl Constrained for Name { reason: "Actors must have uniqueIdentifier AND UID, only UID found".to_string(), }); } + Ok(()) + } +} + +impl ActorConstrained for Name { + fn validate_actor(&self) -> Result<(), ConstraintError> { + self.validate()?; + let mut uid: RelativeDistinguishedName = RelativeDistinguishedName::default(); + let mut vec_dc: Vec = Vec::new(); - // Only check if we are dealing with an actor - if num_uid > 0 && num_unique_identifier > 0 { - vec_dc.reverse(); // The order of the DCs is reversed in the [Name] object, starting with the TLD - - // Check if the domain components are equal between the UID and the DCs - // First, remove the "username@" from the UID - // We can unwrap, because an @ is guaranteed to be included in the string. The regex above - // makes sure of that. - let position_of_at = uid.to_string().find('@').unwrap(); - 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; - for component in dc_normalized_uid.iter() { - debug!("Checking if component \"{}\"...", component); - let equivalent_dc = match vec_dc.get(index as usize) { - Some(dc) => dc, - None => { - return Err(ConstraintError::Malformed(Some( - "Domain Components do not equal the domain components in the UID" - .to_string(), - ))) + 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 => { + uid = rdn.clone(); } - }; - let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); - debug!( - "...is equal to component \"{}\"...", - equivalent_dc.to_string() - ); - if component != &equivalent_dc.to_string() { + OID_RDN_DOMAIN_COMPONENT => { + vec_dc.push(rdn.clone()); + } + _ => {} + } + } + } + vec_dc.reverse(); // The order of the DCs is reversed in the [Name] object, starting with the TLD + + // Check if the domain components are equal between the UID and the DCs + // First, remove the "username@" from the UID + // We can unwrap, because an @ is guaranteed to be included in the string. The regex above + // makes sure of that. + let position_of_at = uid.to_string().find('@').unwrap(); + 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; + for component in dc_normalized_uid.iter() { + debug!("Checking if component \"{}\"...", component); + let equivalent_dc = match vec_dc.get(index as usize) { + Some(dc) => dc, + None => { return Err(ConstraintError::Malformed(Some( "Domain Components do not equal the domain components in the UID" .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(), - ))) - } - }; + }; + let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); + debug!( + "...is equal to component \"{}\"...", + equivalent_dc.to_string() + ); + if component != &equivalent_dc.to_string() { + return Err(ConstraintError::Malformed(Some( + "Domain Components do not equal the domain components in the UID".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(()) } } +pub(super) mod rdn { + use super::*; + use x509_cert::attr::AttributeTypeAndValue; + + pub(super) 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(()) + } + } + + pub(super) fn validate_rdn_unique_identifier( + item: &AttributeTypeAndValue, + ) -> Result<(), ConstraintError> { + if let Ok(value) = Ia5String::new(&String::from_utf8_lossy(item.value.value()).to_string()) + { + SessionId::new_validated(value)?; + Ok(()) + } else { + Err(ConstraintError::Malformed(Some( + "Tried to decode SessionID (uniqueIdentifier) as Ia5String and failed".to_string(), + ))) + } + } +} + 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> { From 71ed426807d7bcd0c1efd69091a7bf4dfa14538d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:21:22 +0200 Subject: [PATCH 059/215] impl ActorConstrained for IdCert --- src/certs/idcert.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 771df48..1db5377 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -13,7 +13,7 @@ use crate::errors::base::InvalidInput; use crate::errors::composite::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; -use crate::Constrained; +use crate::{ActorConstrained, Constrained, HomeServerConstrained}; use super::equal_domain_components; use super::idcerttbs::IdCertTbs; @@ -66,7 +66,7 @@ impl> IdCert { id_cert_tbs, signature, }; - cert.validate()?; + cert.validate_home_server()?; Ok(cert) } @@ -105,7 +105,7 @@ impl> IdCert { id_cert_tbs, signature, }; - cert.validate()?; + cert.validate_actor()?; Ok(cert) } @@ -143,15 +143,6 @@ impl> IdCert { Ok(()) } - /// Validates the well-formedness of the [IdCert] and its contents. Fails, if the [Name] or - /// [Capabilities] do not meet polyproto validation criteria for actor certs, or if - /// the signature fails to be verified. - pub fn validate_actor(&self) -> Result<(), ConversionError> { - self.validate()?; - self.id_cert_tbs.validate_actor()?; - Ok(()) - } - /// 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. /// @@ -163,6 +154,15 @@ impl> IdCert { } } +impl> ActorConstrained for IdCert { + fn validate_actor(&self) -> Result<(), crate::errors::base::ConstraintError> { + self.validate()?; + self.id_cert_tbs.subject.validate_actor()?; + self.id_cert_tbs.validate_actor()?; + Ok(()) + } +} + impl> TryFrom> for Certificate { type Error = ConversionError; fn try_from(value: IdCert) -> Result { From 9c4023f039ce513a9af83a318df5079554eba608 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:22:15 +0200 Subject: [PATCH 060/215] impl HomeServerConstrained for IdCert --- src/certs/idcert.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 1db5377..4083ff4 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -133,16 +133,6 @@ impl> IdCert { Ok(Certificate::try_from(self)?.to_pem(line_ending)?) } - /// Validates the well-formedness of the [IdCert] and its contents. Fails, if the [Name] or - /// [Capabilities] do not meet polyproto validation criteria for home server certs, or if - /// the signature fails to be verified. - // PRETTYFYME: validate_home_server and validate_actor could be made into a trait? - pub fn validate_home_server(&self) -> Result<(), ConversionError> { - self.validate()?; - self.id_cert_tbs.validate_home_server()?; - Ok(()) - } - /// 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. /// @@ -163,6 +153,14 @@ impl> ActorConstrained for IdCert { } } +impl> HomeServerConstrained for IdCert { + fn validate_home_server(&self) -> Result<(), crate::errors::base::ConstraintError> { + self.validate()?; + self.id_cert_tbs.validate_home_server()?; + Ok(()) + } +} + impl> TryFrom> for Certificate { type Error = ConversionError; fn try_from(value: IdCert) -> Result { From a692faa173904d9a3ab39be88eb95477d975ba5c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:23:04 +0200 Subject: [PATCH 061/215] impl ActorConstrained, HomeServerConstrained for IdCertTbs --- src/certs/idcerttbs.rs | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 49b966f..1967c02 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -16,7 +16,7 @@ use crate::errors::base::{ConstraintError, InvalidInput}; use crate::errors::composite::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; -use crate::Constrained; +use crate::{ActorConstrained, Constrained, HomeServerConstrained}; use super::capabilities::Capabilities; use super::idcsr::IdCsr; @@ -145,32 +145,28 @@ impl> IdCertTbs { pub fn from_der(bytes: &[u8]) -> Result { IdCertTbs::try_from(TbsCertificate::from_der(bytes)?) } +} - /// Validates the well-formedness of the [IdCertTbs] and its contents. Fails, if the [Name] or - /// [Capabilities] do not meet polyproto validation criteria for home server certs, or if - /// the signature fails to be verified. - // PRETTYFYME: validate_home_server and validate_actor could be made into a trait? - pub fn validate_home_server(&self) -> Result<(), ConversionError> { +impl> ActorConstrained for IdCertTbs { + fn validate_actor(&self) -> Result<(), ConstraintError> { self.validate()?; - if !self.capabilities.basic_constraints.ca { - return Err(ConversionError::ConstraintError( - ConstraintError::Malformed(Some( - "Home server cert must have the CA capability set to true".to_string(), - )), - )); + self.subject.validate_actor()?; + if self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Actor cert must not be a CA".to_string(), + ))); } Ok(()) } +} - /// Validates the well-formedness of the [IdCertTbs] and its contents. Fails, if the [Name] or - /// [Capabilities] do not meet polyproto validation criteria for actor certs, or if - /// the signature fails to be verified. - pub fn validate_actor(&self) -> Result<(), ConversionError> { +impl> HomeServerConstrained for IdCertTbs { + fn validate_home_server(&self) -> Result<(), ConstraintError> { self.validate()?; - if self.capabilities.basic_constraints.ca { - return Err(ConversionError::ConstraintError( - ConstraintError::Malformed(Some("Actor cert must not be a CA".to_string())), - )); + if !self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Home server cert must have the CA capability set to true".to_string(), + ))); } Ok(()) } From 7bc9095e400271a928cd4fd61385c1b5400dabcd Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:23:41 +0200 Subject: [PATCH 062/215] move CSR test to tests/idcsr.rs --- tests/idcert.rs | 27 --------------------------- tests/idcsr.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/idcert.rs b/tests/idcert.rs index 5289b4e..06117a3 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -128,33 +128,6 @@ fn test_create_ca_cert() { 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_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=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", - ) - .unwrap(), - &priv_key, - &capabilities, - ); - assert!(csr.is_err()); -} - #[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() { diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 1413124..59346ec 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -14,7 +14,7 @@ use std::time::Duration; use der::asn1::{BitString, Ia5String, Uint, UtcTime}; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; -use polyproto::certs::capabilities::Capabilities; +use polyproto::certs::capabilities::{self, Capabilities}; use polyproto::certs::idcert::IdCert; use polyproto::certs::idcsr::IdCsr; use polyproto::certs::PublicKeyInfo; @@ -66,6 +66,33 @@ fn csr_from_pem() { assert_eq!(csr_from_der, 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() { + 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, + ); + 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() { From 76b9c8f16a3072466c9a7965ba520d317f2eedcc Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:43:00 +0200 Subject: [PATCH 063/215] Add `target` parameter to from_pem and from_der methods --- src/api/identity/mod.rs | 5 ++++- src/certs/idcert.rs | 22 +++++++++++++++++----- src/certs/idcerttbs.rs | 14 +++++++++++--- src/certs/idcsr.rs | 26 +++++++++++++++++++++----- src/certs/mod.rs | 6 ++++++ src/types/schemas/authentication.rs | 2 +- tests/idcert.rs | 2 +- tests/idcsr.rs | 6 ++++-- 8 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/api/identity/mod.rs b/src/api/identity/mod.rs index 2dcef4e..84897d0 100644 --- a/src/api/identity/mod.rs +++ b/src/api/identity/mod.rs @@ -44,6 +44,9 @@ impl HttpClient { ) .await?; - Ok(IdCert::from_pem(response.text().await?.as_str())?) + Ok(IdCert::from_pem( + response.text().await?.as_str(), + Some(crate::certs::Target::HomeServer), + )?) } } diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 4083ff4..a163082 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -15,9 +15,9 @@ use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; use crate::{ActorConstrained, Constrained, HomeServerConstrained}; -use super::equal_domain_components; use super::idcerttbs::IdCertTbs; use super::idcsr::IdCsr; +use super::{equal_domain_components, Target}; /// A signed polyproto ID-Cert, consisting of the actual certificate, the CA-generated signature and /// metadata about that signature. @@ -110,9 +110,15 @@ impl> IdCert { } /// Create an IdCsr from a byte slice containing a DER encoded X.509 Certificate. - pub fn from_der(value: &[u8]) -> Result { + pub fn from_der(value: &[u8], target: Option) -> Result { let cert = IdCert::try_from(Certificate::from_der(value)?)?; - cert.validate()?; + match target { + Some(choice) => match choice { + Target::Actor => cert.validate_actor()?, + Target::HomeServer => cert.validate_home_server()?, + }, + None => todo!(), + } Ok(cert) } @@ -122,9 +128,15 @@ impl> IdCert { } /// Create an IdCsr from a byte slice containing a PEM encoded X.509 Certificate. - pub fn from_pem(pem: &str) -> Result { + pub fn from_pem(pem: &str, target: Option) -> Result { let cert = IdCert::try_from(Certificate::from_pem(pem)?)?; - cert.validate()?; + match target { + Some(choice) => match choice { + Target::Actor => cert.validate_actor()?, + Target::HomeServer => cert.validate_home_server()?, + }, + None => cert.validate()?, + } Ok(cert) } diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 1967c02..46fa4db 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -20,7 +20,7 @@ use crate::{ActorConstrained, Constrained, HomeServerConstrained}; use super::capabilities::Capabilities; use super::idcsr::IdCsr; -use super::PublicKeyInfo; +use super::{PublicKeyInfo, Target}; /// An unsigned polyproto ID-Cert. /// @@ -142,8 +142,16 @@ impl> IdCertTbs { } /// 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)?) + pub fn from_der(bytes: &[u8], target: Option) -> Result { + let cert = IdCertTbs::try_from(TbsCertificate::from_der(bytes)?)?; + match target { + Some(choice) => match choice { + Target::Actor => cert.validate_actor()?, + Target::HomeServer => cert.validate_home_server()?, + }, + None => cert.validate()?, + }; + Ok(cert) } } diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index d9c0db0..84b5687 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -17,7 +17,7 @@ use crate::signature::Signature; use crate::{Constrained, ConstraintError}; 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). @@ -108,8 +108,16 @@ impl> IdCsr { /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. // PRETTYFYME: Could be a trait along with to_der, from_pem, to_pem - pub fn from_der(bytes: &[u8]) -> Result { - IdCsr::try_from(CertReq::from_der(bytes)?) + pub fn from_der(bytes: &[u8], target: Option) -> Result { + let csr = IdCsr::try_from(CertReq::from_der(bytes)?)?; + match target { + Some(choice) => match choice { + Target::Actor => csr.validate_actor()?, + Target::HomeServer => csr.validate_home_server()?, + }, + None => csr.validate()?, + }; + Ok(csr) } /// Encode this type as DER, returning a byte vector. @@ -118,8 +126,16 @@ impl> IdCsr { } /// Create an IdCsr from a string containing a PEM encoded PKCS #10 CSR. - pub fn from_pem(pem: &str) -> Result { - IdCsr::try_from(CertReq::from_pem(pem)?) + pub fn from_pem(pem: &str, target: Option) -> Result { + let csr = IdCsr::try_from(CertReq::from_pem(pem)?)?; + match target { + Some(choice) => match choice { + Target::Actor => csr.validate_actor()?, + Target::HomeServer => csr.validate_home_server()?, + }, + None => csr.validate()?, + }; + Ok(csr) } /// Encode this type as PEM, returning a string. diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 141839f..822bdd2 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -58,6 +58,12 @@ impl SessionId { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Target { + Actor, + HomeServer, +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(u8)] /// `PKCS#10` version. From the PKCS specification document (RFC 2986): diff --git a/src/types/schemas/authentication.rs b/src/types/schemas/authentication.rs index df9451e..38c2970 100644 --- a/src/types/schemas/authentication.rs +++ b/src/types/schemas/authentication.rs @@ -36,7 +36,7 @@ impl> TryFrom for IdCert Result { - Self::from_pem(&value.id_cert) + Self::from_pem(&value.id_cert, Some(crate::certs::Target::Actor)) } } diff --git a/tests/idcert.rs b/tests/idcert.rs index 06117a3..c1a8aab 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -204,7 +204,7 @@ fn cert_from_pem() { ) .unwrap(); let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_der = IdCert::from_pem(&data).unwrap(); + let cert_from_der = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(cert_from_der, cert) } diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 59346ec..3e6104b 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -29,6 +29,8 @@ use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; use x509_cert::Certificate; +// TODO: Add test where CSR DC and Cert DC are different(?) + /// 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. @@ -62,7 +64,7 @@ fn csr_from_pem() { ) .unwrap(); let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let csr_from_der = IdCsr::from_pem(&data).unwrap(); + let csr_from_der = IdCsr::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(csr_from_der, csr) } @@ -109,7 +111,7 @@ fn csr_from_der() { ) .unwrap(); let data = csr.clone().to_der().unwrap(); - let csr_from_der = IdCsr::from_der(&data).unwrap(); + let csr_from_der = IdCsr::from_der(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(csr_from_der, csr) } From 8c6f9f45e3bfa816a4721da4ace02b56bb8b89d8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 19:55:16 +0200 Subject: [PATCH 064/215] Fix tests :tada: --- examples/ed25519_cert.rs | 7 +++++-- examples/ed25519_from_der.rs | 9 ++++++--- src/constraints.rs | 6 +++--- tests/idcert.rs | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index f251d2b..cc46b9b 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -51,7 +51,10 @@ 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(), ) @@ -66,7 +69,7 @@ fn main() { &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", + "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", ) .unwrap(), Validity { diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index d172041..93a08ee 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -44,7 +44,10 @@ 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(), ) @@ -60,7 +63,7 @@ fn main() { &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", + "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", ) .unwrap(), Validity { @@ -74,7 +77,7 @@ fn main() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); - let cert_from_der = IdCert::from_der(&data).unwrap(); + let cert_from_der = IdCert::from_der(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(cert_from_der, cert) } diff --git a/src/constraints.rs b/src/constraints.rs index 048dac3..55e0935 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -372,7 +372,7 @@ mod name_constraints { use x509_cert::name::Name; use crate::testing_utils::init_logger; - use crate::Constrained; + use crate::{ActorConstrained, Constrained}; #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -400,13 +400,13 @@ mod name_constraints { "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().err().unwrap(); + name.validate_actor().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().err().unwrap(); + name.validate_actor().err().unwrap(); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] diff --git a/tests/idcert.rs b/tests/idcert.rs index c1a8aab..69e262e 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -139,7 +139,7 @@ fn mismatched_dcs_in_csr_and_cert() { let csr = polyproto::certs::idcsr::IdCsr::new( &RdnSequence::from_str( - "CN=flori,DC=example,DC=com,UID=flori@example.com,uniqueIdentifier=client1", + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyproto.chat,uniqueIdentifier=client1", ) .unwrap(), &priv_key, From ab3b20201617a3e2b089a12d75411d4ec17935d1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 20:22:06 +0200 Subject: [PATCH 065/215] Add `target` parameter to `IdCert::new()` --- examples/ed25519_cert.rs | 3 ++- examples/ed25519_from_der.rs | 1 + src/certs/idcsr.rs | 43 +++++++++++++++++++++++++++++++----- src/constraints.rs | 13 ++++++++--- tests/idcert.rs | 8 +++++-- tests/idcsr.rs | 5 ++++- 6 files changed, 60 insertions(+), 13 deletions(-) diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index cc46b9b..4a1b44c 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -12,7 +12,7 @@ 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; @@ -57,6 +57,7 @@ fn main() { .unwrap(), &priv_key, &Capabilities::default_actor(), + Some(Target::Actor), ) .unwrap(); let data = csr.clone().to_der().unwrap(); diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index 93a08ee..07a0de1 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -50,6 +50,7 @@ fn main() { .unwrap(), &priv_key, &Capabilities::default_actor(), + Some(polyproto::certs::Target::Actor), ) .unwrap(); diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 84b5687..2be48a5 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -14,7 +14,7 @@ use x509_cert::request::{CertReq, CertReqInfo}; use crate::errors::composite::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; -use crate::{Constrained, ConstraintError}; +use crate::{ActorConstrained, Constrained, ConstraintError}; use super::capabilities::Capabilities; use super::{PkcsVersion, PublicKeyInfo, Target}; @@ -63,9 +63,17 @@ impl> IdCsr { subject: &Name, signing_key: &impl PrivateKey, capabilities: &Capabilities, + target: Option, ) -> Result, ConversionError> { - subject.validate()?; - let inner_csr = IdCsrInner::::new(subject, signing_key.pubkey(), capabilities)?; + match target { + Some(choice) => match choice { + Target::Actor => subject.validate_actor()?, + Target::HomeServer => subject.validate()?, + }, + None => subject.validate()?, + } + let inner_csr = + IdCsrInner::::new(subject, signing_key.pubkey(), capabilities, target)?; let signature = signing_key.sign(&inner_csr.clone().to_der()?); let signature_algorithm = S::algorithm_identifier(); @@ -185,8 +193,15 @@ impl> IdCsrInner { subject: &Name, public_key: &P, capabilities: &Capabilities, + target: Option, ) -> Result, ConversionError> { - subject.validate()?; + match target { + Some(choice) => match choice { + Target::Actor => subject.validate_actor()?, + Target::HomeServer => subject.validate()?, + }, + None => todo!(), + } capabilities.validate()?; let subject = subject.clone(); @@ -202,8 +217,16 @@ impl> IdCsrInner { } /// 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)?) + pub fn from_der(bytes: &[u8], target: Option) -> Result { + let csr_inner = IdCsrInner::try_from(CertReqInfo::from_der(bytes)?)?; + match target { + Some(choice) => match choice { + Target::Actor => csr_inner.validate_actor()?, + Target::HomeServer => csr_inner.validate()?, + }, + None => csr_inner.validate()?, + }; + Ok(csr_inner) } /// Encode this type as DER, returning a byte vector. @@ -212,6 +235,14 @@ impl> IdCsrInner { } } +impl> ActorConstrained for IdCsrInner { + fn validate_actor(&self) -> Result<(), ConstraintError> { + self.validate()?; + self.subject.validate_actor()?; + Ok(()) + } +} + impl> TryFrom for IdCsr { type Error = ConversionError; diff --git a/src/constraints.rs b/src/constraints.rs index 55e0935..2a6cfb3 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -11,7 +11,7 @@ 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; +use crate::certs::idcsr::{IdCsr, IdCsrInner}; use crate::certs::{equal_domain_components, SessionId}; use crate::errors::base::ConstraintError; use crate::key::PublicKey; @@ -308,10 +308,17 @@ impl Constrained for Capabilities { } } +impl> Constrained for IdCsrInner { + fn validate(&self) -> Result<(), ConstraintError> { + self.capabilities.validate()?; + self.subject.validate()?; + Ok(()) + } +} + impl> Constrained for IdCsr { fn validate(&self) -> Result<(), ConstraintError> { - self.inner_csr.capabilities.validate()?; - self.inner_csr.subject.validate()?; + self.inner_csr.validate()?; match self.inner_csr.subject_public_key.verify_signature( &self.signature, match &self.inner_csr.clone().to_der() { diff --git a/tests/idcert.rs b/tests/idcert.rs index 69e262e..b6e9416 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -12,7 +12,7 @@ use der::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; +use polyproto::certs::{PublicKeyInfo, Target}; use polyproto::errors::composite::ConversionError; use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; @@ -62,6 +62,7 @@ fn test_create_actor_cert() { .unwrap(), &priv_key, &capabilities, + Some(Target::Actor), ) .unwrap(); let cert = IdCert::from_actor_csr( @@ -103,6 +104,7 @@ fn test_create_ca_cert() { .unwrap(), &priv_key, &Capabilities::default_home_server(), + Some(Target::HomeServer), ) .unwrap(); let cert = IdCert::from_ca_csr( @@ -139,11 +141,12 @@ fn mismatched_dcs_in_csr_and_cert() { let csr = polyproto::certs::idcsr::IdCsr::new( &RdnSequence::from_str( - "CN=flori,DC=polyphony,DC=chat,UID=flori@polyproto.chat,uniqueIdentifier=client1", + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", ) .unwrap(), &priv_key, &Capabilities::default_home_server(), + Some(Target::Actor), ) .unwrap(); let cert = IdCert::from_ca_csr( @@ -182,6 +185,7 @@ fn cert_from_pem() { .unwrap(), &priv_key, &Capabilities::default_actor(), + Some(Target::Actor), ) .unwrap(); diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 3e6104b..255c160 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -17,7 +17,7 @@ use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, Veri use polyproto::certs::capabilities::{self, Capabilities}; use polyproto::certs::idcert::IdCert; use polyproto::certs::idcsr::IdCsr; -use polyproto::certs::PublicKeyInfo; +use polyproto::certs::{PublicKeyInfo, Target}; use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; use rand::rngs::OsRng; @@ -61,6 +61,7 @@ fn csr_from_pem() { .unwrap(), &priv_key, &Capabilities::default_actor(), + Some(Target::Actor), ) .unwrap(); let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); @@ -91,6 +92,7 @@ fn test_create_invalid_actor_csr() { .unwrap(), &priv_key, &capabilities, + Some(Target::Actor), ); assert!(csr.is_err()); } @@ -108,6 +110,7 @@ fn csr_from_der() { .unwrap(), &priv_key, &Capabilities::default_actor(), + Some(Target::Actor), ) .unwrap(); let data = csr.clone().to_der().unwrap(); From 0c6230c8a9977b74268031c819c0c8220e5fdc01 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 20:59:43 +0200 Subject: [PATCH 066/215] impl actorconstrained, homeserverconstrained for idcsr --- src/certs/idcsr.rs | 56 +++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 2be48a5..c3e2be5 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -14,7 +14,7 @@ use x509_cert::request::{CertReq, CertReqInfo}; use crate::errors::composite::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; -use crate::{ActorConstrained, Constrained, ConstraintError}; +use crate::{ActorConstrained, Constrained, ConstraintError, HomeServerConstrained}; use super::capabilities::Capabilities; use super::{PkcsVersion, PublicKeyInfo, Target}; @@ -84,36 +84,6 @@ impl> IdCsr { }) } - /// 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 validate_actor(&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(()) - } - - /// 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 validate_home_server(&self) -> Result<(), ConversionError> { - self.validate()?; - // TODO: There are missing constraints here. The home server CSR must not have a UID/uniqueIdentifier - // field set, for example. - if !self.inner_csr.capabilities.basic_constraints.ca { - return Err(ConversionError::ConstraintError( - ConstraintError::Malformed(Some( - "Home server CSR must have the CA capability set to true".to_string(), - )), - )); - } - Ok(()) - } - /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. // PRETTYFYME: Could be a trait along with to_der, from_pem, to_pem pub fn from_der(bytes: &[u8], target: Option) -> Result { @@ -162,6 +132,30 @@ impl> IdCsr { } } +impl> ActorConstrained for IdCsr { + fn validate_actor(&self) -> Result<(), ConstraintError> { + self.validate()?; + if self.inner_csr.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Actor CSR must not be a CA".to_string(), + ))); + } + Ok(()) + } +} + +impl> HomeServerConstrained for IdCsr { + fn validate_home_server(&self) -> Result<(), ConstraintError> { + self.validate()?; + if !self.inner_csr.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Home server CSR must have the CA capability set to true".to_string(), + ))); + } + Ok(()) + } +} + /// In the context of PKCS #10, this is a `CertificationRequestInfo`: /// /// ```md From 5cc8a45e003066b695bea96508359510be3a048d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 22:11:20 +0200 Subject: [PATCH 067/215] Create directory constraints/ --- src/constraints.rs | 514 ----------------------------------------- src/constraints/mod.rs | 220 ++++++++++++++++++ 2 files changed, 220 insertions(+), 514 deletions(-) delete mode 100644 src/constraints.rs create mode 100644 src/constraints/mod.rs diff --git a/src/constraints.rs b/src/constraints.rs deleted file mode 100644 index 2a6cfb3..0000000 --- a/src/constraints.rs +++ /dev/null @@ -1,514 +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 log::debug; -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}; -use crate::errors::base::ConstraintError; -use crate::key::PublicKey; -use crate::signature::Signature; -use crate::{ - ActorConstrained, Constrained, OID_RDN_COMMON_NAME, OID_RDN_DOMAIN_COMPONENT, OID_RDN_UID, - OID_RDN_UNIQUE_IDENTIFIER, -}; - -use self::rdn::{validate_rdn_uid, validate_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 mut vec_dc: Vec = Vec::new(); - - 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; - validate_rdn_uid(item)?; - } - OID_RDN_UNIQUE_IDENTIFIER => { - num_unique_identifier += 1; - validate_rdn_unique_identifier(item)?; - } - 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; - vec_dc.push(rdn.clone()); - } - _ => {} - } - } - } - 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 ActorConstrained for Name { - fn validate_actor(&self) -> Result<(), ConstraintError> { - self.validate()?; - let mut uid: RelativeDistinguishedName = RelativeDistinguishedName::default(); - let mut vec_dc: Vec = Vec::new(); - - 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 => { - uid = rdn.clone(); - } - OID_RDN_DOMAIN_COMPONENT => { - vec_dc.push(rdn.clone()); - } - _ => {} - } - } - } - vec_dc.reverse(); // The order of the DCs is reversed in the [Name] object, starting with the TLD - - // Check if the domain components are equal between the UID and the DCs - // First, remove the "username@" from the UID - // We can unwrap, because an @ is guaranteed to be included in the string. The regex above - // makes sure of that. - let position_of_at = uid.to_string().find('@').unwrap(); - 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; - for component in dc_normalized_uid.iter() { - debug!("Checking if component \"{}\"...", component); - let equivalent_dc = match vec_dc.get(index as usize) { - Some(dc) => dc, - None => { - return Err(ConstraintError::Malformed(Some( - "Domain Components do not equal the domain components in the UID" - .to_string(), - ))) - } - }; - let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); - debug!( - "...is equal to component \"{}\"...", - equivalent_dc.to_string() - ); - if component != &equivalent_dc.to_string() { - return Err(ConstraintError::Malformed(Some( - "Domain Components do not equal the domain components in the UID".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(()) - } -} - -pub(super) mod rdn { - use super::*; - use x509_cert::attr::AttributeTypeAndValue; - - pub(super) 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(()) - } - } - - pub(super) fn validate_rdn_unique_identifier( - item: &AttributeTypeAndValue, - ) -> Result<(), ConstraintError> { - if let Ok(value) = Ia5String::new(&String::from_utf8_lossy(item.value.value()).to_string()) - { - SessionId::new_validated(value)?; - Ok(()) - } else { - Err(ConstraintError::Malformed(Some( - "Tried to decode SessionID (uniqueIdentifier) as Ia5String and failed".to_string(), - ))) - } - } -} - -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 IdCsrInner { - fn validate(&self) -> Result<(), ConstraintError> { - self.capabilities.validate()?; - self.subject.validate()?; - Ok(()) - } -} - -impl> Constrained for IdCsr { - fn validate(&self) -> Result<(), ConstraintError> { - self.inner_csr.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::testing_utils::init_logger; - use crate::{ActorConstrained, 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(); - name.validate().unwrap(); - let name = Name::from_str("CN=flori,DC=www,DC=polyphony,DC=chat").unwrap(); - name.validate().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().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 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_actor().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_actor().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 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() { - init_logger(); - 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() { - init_logger(); - 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() { - init_logger(); - 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() { - init_logger(); - 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() { - init_logger(); - 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/mod.rs b/src/constraints/mod.rs new file mode 100644 index 0000000..3019074 --- /dev/null +++ b/src/constraints/mod.rs @@ -0,0 +1,220 @@ +// 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, 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::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, +}; + +pub mod capabilities; +pub mod certs; +pub mod name; +pub mod session_id; + +#[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 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()) + } +} From 1eb9c788adaed398fe82698f95745cfe93ac5c5f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 22:11:54 +0200 Subject: [PATCH 068/215] Remove ActorConstrained and HomeServerConstrained for better Constrained::validate() API --- src/certs/capabilities/mod.rs | 2 +- src/certs/idcert.rs | 47 ++---- src/certs/idcerttbs.rs | 47 +----- src/certs/idcsr.rs | 78 +--------- src/certs/mod.rs | 2 +- src/constraints/capabilities.rs | 83 ++++++++++ src/constraints/certs.rs | 105 +++++++++++++ src/constraints/name.rs | 202 +++++++++++++++++++++++++ src/constraints/session_id.rs | 20 +++ src/lib.rs | 37 +---- src/types/entities/challenge_string.rs | 6 +- tests/idcert.rs | 14 +- 12 files changed, 454 insertions(+), 189 deletions(-) create mode 100644 src/constraints/capabilities.rs create mode 100644 src/constraints/certs.rs create mode 100644 src/constraints/name.rs create mode 100644 src/constraints/session_id.rs diff --git a/src/certs/capabilities/mod.rs b/src/certs/capabilities/mod.rs index a0a583f..7f35c6e 100644 --- a/src/certs/capabilities/mod.rs +++ b/src/certs/capabilities/mod.rs @@ -140,7 +140,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 a163082..628e40a 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -13,7 +13,7 @@ use crate::errors::base::InvalidInput; use crate::errors::composite::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; -use crate::{ActorConstrained, Constrained, HomeServerConstrained}; +use crate::Constrained; use super::idcerttbs::IdCertTbs; use super::idcsr::IdCsr; @@ -66,7 +66,7 @@ impl> IdCert { id_cert_tbs, signature, }; - cert.validate_home_server()?; + cert.validate(Some(Target::HomeServer))?; Ok(cert) } @@ -85,7 +85,7 @@ impl> IdCert { ) -> Result { // IdCsr gets validated in IdCertTbs::from_..._csr let signature_algorithm = signing_key.algorithm_identifier(); - issuer.validate()?; + issuer.validate(Some(Target::Actor))?; if !equal_domain_components(&id_csr.inner_csr.subject, &issuer) { return Err(ConversionError::InvalidInput( crate::errors::base::InvalidInput::Malformed( @@ -105,20 +105,14 @@ impl> IdCert { id_cert_tbs, signature, }; - cert.validate_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: &[u8], target: Option) -> Result { let cert = IdCert::try_from(Certificate::from_der(value)?)?; - match target { - Some(choice) => match choice { - Target::Actor => cert.validate_actor()?, - Target::HomeServer => cert.validate_home_server()?, - }, - None => todo!(), - } + cert.validate(target)?; Ok(cert) } @@ -130,13 +124,7 @@ impl> IdCert { /// Create an IdCsr from a byte slice containing a PEM encoded X.509 Certificate. pub fn from_pem(pem: &str, target: Option) -> Result { let cert = IdCert::try_from(Certificate::from_pem(pem)?)?; - match target { - Some(choice) => match choice { - Target::Actor => cert.validate_actor()?, - Target::HomeServer => cert.validate_home_server()?, - }, - None => cert.validate()?, - } + cert.validate(target)?; Ok(cert) } @@ -156,23 +144,6 @@ impl> IdCert { } } -impl> ActorConstrained for IdCert { - fn validate_actor(&self) -> Result<(), crate::errors::base::ConstraintError> { - self.validate()?; - self.id_cert_tbs.subject.validate_actor()?; - self.id_cert_tbs.validate_actor()?; - Ok(()) - } -} - -impl> HomeServerConstrained for IdCert { - fn validate_home_server(&self) -> Result<(), crate::errors::base::ConstraintError> { - self.validate()?; - self.id_cert_tbs.validate_home_server()?; - Ok(()) - } -} - impl> TryFrom> for Certificate { type Error = ConversionError; fn try_from(value: IdCert) -> Result { @@ -190,9 +161,11 @@ impl> TryFrom for IdCert { fn try_from(value: Certificate) -> Result { let id_cert_tbs = value.tbs_certificate.try_into()?; let signature = S::from_bytes(value.signature.raw_bytes()); - Ok(IdCert { + let cert = IdCert { id_cert_tbs, signature, - }) + }; + cert.validate(None)?; + Ok(cert) } } diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 46fa4db..3ce54de 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -12,11 +12,11 @@ use x509_cert::serial_number::SerialNumber; use x509_cert::time::Validity; use x509_cert::TbsCertificate; -use crate::errors::base::{ConstraintError, InvalidInput}; +use crate::errors::base::InvalidInput; use crate::errors::composite::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; -use crate::{ActorConstrained, Constrained, HomeServerConstrained}; +use crate::Constrained; use super::capabilities::Capabilities; use super::idcsr::IdCsr; @@ -81,8 +81,8 @@ impl> IdCertTbs { "Actor ID-Cert cannot have \"CA\" BasicConstraint set to true".to_string(), ))); } - id_csr.validate()?; - issuer.validate()?; + id_csr.validate(Some(Target::Actor))?; + issuer.validate(Some(Target::Actor))?; // Verify if signature of IdCsr matches contents id_csr.inner_csr.subject_public_key.verify_signature( &id_csr.signature, @@ -118,7 +118,7 @@ impl> IdCertTbs { "CA ID-Cert must have \"CA\" BasicConstraint set to true".to_string(), ))); } - id_csr.validate()?; + id_csr.validate(Some(Target::HomeServer))?; // Verify if signature of IdCsr matches contents id_csr.inner_csr.subject_public_key.verify_signature( &id_csr.signature, @@ -142,51 +142,20 @@ impl> IdCertTbs { } /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. - pub fn from_der(bytes: &[u8], target: Option) -> Result { + pub fn from_der(bytes: &[u8], target: Target) -> Result { let cert = IdCertTbs::try_from(TbsCertificate::from_der(bytes)?)?; - match target { - Some(choice) => match choice { - Target::Actor => cert.validate_actor()?, - Target::HomeServer => cert.validate_home_server()?, - }, - None => cert.validate()?, - }; + cert.validate(Some(target))?; Ok(cert) } } -impl> ActorConstrained for IdCertTbs { - fn validate_actor(&self) -> Result<(), ConstraintError> { - self.validate()?; - self.subject.validate_actor()?; - if self.capabilities.basic_constraints.ca { - return Err(ConstraintError::Malformed(Some( - "Actor cert must not be a CA".to_string(), - ))); - } - Ok(()) - } -} - -impl> HomeServerConstrained for IdCertTbs { - fn validate_home_server(&self) -> Result<(), ConstraintError> { - self.validate()?; - if !self.capabilities.basic_constraints.ca { - return Err(ConstraintError::Malformed(Some( - "Home server cert must have the CA capability set to true".to_string(), - ))); - } - Ok(()) - } -} - impl> TryFrom> for IdCertTbs { type Error = ConversionError; 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 c3e2be5..dc44633 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -14,7 +14,7 @@ use x509_cert::request::{CertReq, CertReqInfo}; use crate::errors::composite::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; -use crate::{ActorConstrained, Constrained, ConstraintError, HomeServerConstrained}; +use crate::Constrained; use super::capabilities::Capabilities; use super::{PkcsVersion, PublicKeyInfo, Target}; @@ -65,13 +65,7 @@ impl> IdCsr { capabilities: &Capabilities, target: Option, ) -> Result, ConversionError> { - match target { - Some(choice) => match choice { - Target::Actor => subject.validate_actor()?, - Target::HomeServer => subject.validate()?, - }, - None => subject.validate()?, - } + subject.validate(target)?; let inner_csr = IdCsrInner::::new(subject, signing_key.pubkey(), capabilities, target)?; let signature = signing_key.sign(&inner_csr.clone().to_der()?); @@ -88,13 +82,7 @@ impl> IdCsr { // PRETTYFYME: Could be a trait along with to_der, from_pem, to_pem pub fn from_der(bytes: &[u8], target: Option) -> Result { let csr = IdCsr::try_from(CertReq::from_der(bytes)?)?; - match target { - Some(choice) => match choice { - Target::Actor => csr.validate_actor()?, - Target::HomeServer => csr.validate_home_server()?, - }, - None => csr.validate()?, - }; + csr.validate(target)?; Ok(csr) } @@ -106,13 +94,7 @@ impl> IdCsr { /// Create an IdCsr from a string containing a PEM encoded PKCS #10 CSR. pub fn from_pem(pem: &str, target: Option) -> Result { let csr = IdCsr::try_from(CertReq::from_pem(pem)?)?; - match target { - Some(choice) => match choice { - Target::Actor => csr.validate_actor()?, - Target::HomeServer => csr.validate_home_server()?, - }, - None => csr.validate()?, - }; + csr.validate(target)?; Ok(csr) } @@ -132,30 +114,6 @@ impl> IdCsr { } } -impl> ActorConstrained for IdCsr { - fn validate_actor(&self) -> Result<(), ConstraintError> { - self.validate()?; - if self.inner_csr.capabilities.basic_constraints.ca { - return Err(ConstraintError::Malformed(Some( - "Actor CSR must not be a CA".to_string(), - ))); - } - Ok(()) - } -} - -impl> HomeServerConstrained for IdCsr { - fn validate_home_server(&self) -> Result<(), ConstraintError> { - self.validate()?; - if !self.inner_csr.capabilities.basic_constraints.ca { - return Err(ConstraintError::Malformed(Some( - "Home server CSR must have the CA capability set to true".to_string(), - ))); - } - Ok(()) - } -} - /// In the context of PKCS #10, this is a `CertificationRequestInfo`: /// /// ```md @@ -189,14 +147,8 @@ impl> IdCsrInner { capabilities: &Capabilities, target: Option, ) -> Result, ConversionError> { - match target { - Some(choice) => match choice { - Target::Actor => subject.validate_actor()?, - Target::HomeServer => subject.validate()?, - }, - None => todo!(), - } - capabilities.validate()?; + subject.validate(target)?; + capabilities.validate(target)?; let subject = subject.clone(); let subject_public_key_info = public_key.clone(); @@ -213,13 +165,7 @@ impl> IdCsrInner { /// Create an IdCsrInner from a byte slice containing a DER encoded PKCS #10 CSR. pub fn from_der(bytes: &[u8], target: Option) -> Result { let csr_inner = IdCsrInner::try_from(CertReqInfo::from_der(bytes)?)?; - match target { - Some(choice) => match choice { - Target::Actor => csr_inner.validate_actor()?, - Target::HomeServer => csr_inner.validate()?, - }, - None => csr_inner.validate()?, - }; + csr_inner.validate(target)?; Ok(csr_inner) } @@ -229,14 +175,6 @@ impl> IdCsrInner { } } -impl> ActorConstrained for IdCsrInner { - fn validate_actor(&self) -> Result<(), ConstraintError> { - self.validate()?; - self.subject.validate_actor()?; - Ok(()) - } -} - impl> TryFrom for IdCsr { type Error = ConversionError; @@ -254,7 +192,7 @@ impl> TryFrom for IdCsrInner { 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, diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 822bdd2..3828bbd 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -53,7 +53,7 @@ impl SessionId { /// been violated. pub fn new_validated(id: Ia5String) -> Result { let session_id = SessionId { session_id: id }; - session_id.validate()?; + session_id.validate(None)?; Ok(session_id) } } diff --git a/src/constraints/capabilities.rs b/src/constraints/capabilities.rs new file mode 100644 index 0000000..f8f9c7b --- /dev/null +++ b/src/constraints/capabilities.rs @@ -0,0 +1,83 @@ +// 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 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( + "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(()) + } + } +} diff --git a/src/constraints/certs.rs b/src/constraints/certs.rs new file mode 100644 index 0000000..4df5cc7 --- /dev/null +++ b/src/constraints/certs.rs @@ -0,0 +1,105 @@ +// 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 IdCsrInner { + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + self.capabilities.validate(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( + "Actor CSR must not be a CA".to_string(), + ))); + } + } + Target::HomeServer => { + if !self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Home server CSR must have the CA capability set to true".to_string(), + ))); + } + } + } + } + Ok(()) + } +} + +impl> Constrained for IdCsr { + fn validate(&self, target: Option) -> Result<(), ConstraintError> { + self.inner_csr.validate(target)?; + 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, target: Option) -> Result<(), ConstraintError> { + self.id_cert_tbs.validate(target)?; + 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, target: Option) -> Result<(), ConstraintError> { + self.capabilities.validate(target)?; + self.issuer.validate(target)?; + self.subject.validate(target)?; + 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(), + ))) + } + } + if let Some(target) = target { + match target { + Target::Actor => { + if self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Actor cert must not be a CA".to_string(), + ))); + } + } + Target::HomeServer => { + if !self.capabilities.basic_constraints.ca { + return Err(ConstraintError::Malformed(Some( + "Home server cert must have the CA capability set to true".to_string(), + ))); + } + } + } + } + Ok(()) + } +} diff --git a/src/constraints/name.rs b/src/constraints/name.rs new file mode 100644 index 0000000..eba66c3 --- /dev/null +++ b/src/constraints/name.rs @@ -0,0 +1,202 @@ +// 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 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, target: Option) -> 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 mut vec_dc: Vec = Vec::new(); + let mut uid: RelativeDistinguishedName = RelativeDistinguishedName::default(); + + 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; + uid = rdn.clone(); + validate_rdn_uid(item)?; + } + OID_RDN_UNIQUE_IDENTIFIER => { + num_unique_identifier += 1; + validate_rdn_unique_identifier(item)?; + } + 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; + vec_dc.push(rdn.clone()); + } + _ => {} + } + } + } + // 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 => 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)?; + } + + 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(()) + } +} + +/// Check if the domain components are equal between the UID and the DCs +fn validate_dc_matches_dc_in_uid( + vec_dc: Vec, + 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 => { + 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() { + debug!("Checking if component \"{}\"...", component); + let equivalent_dc = match vec_dc.get(index as usize) { + Some(dc) => dc, + None => { + return Err(ConstraintError::Malformed(Some( + "Domain Components do not equal the domain components in the UID".to_string(), + ))) + } + }; + let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); + debug!( + "...is equal to component \"{}\"...", + equivalent_dc.to_string() + ); + if component != &equivalent_dc.to_string() { + return Err(ConstraintError::Malformed(Some( + "Domain Components do not equal the domain components in the UID".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(()) +} + +use log::debug; +use x509_cert::attr::AttributeTypeAndValue; + +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(()) + } +} + +fn validate_rdn_unique_identifier(item: &AttributeTypeAndValue) -> Result<(), ConstraintError> { + if let Ok(value) = Ia5String::new(&String::from_utf8_lossy(item.value.value()).to_string()) { + SessionId::new_validated(value)?; + Ok(()) + } else { + Err(ConstraintError::Malformed(Some( + "Tried to decode SessionID (uniqueIdentifier) as Ia5String and failed".to_string(), + ))) + } +} 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/lib.rs b/src/lib.rs index 24397b9..ac0c44a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ pub const OID_RDN_COMMON_NAME: &str = "2.5.4.3"; pub const OID_RDN_UNIQUE_IDENTIFIER: &str = "0.9.2342.19200300.100.1.44"; pub const OID_RDN_UID: &str = "0.9.2342.19200300.100.1.1"; +use certs::Target; use errors::base::ConstraintError; #[cfg(feature = "reqwest")] @@ -67,7 +68,7 @@ pub mod signature; /// Types used in polyproto and the polyproto HTTP/REST APIs pub mod types; -pub(crate) mod constraints; +pub mod constraints; pub use der; pub use spki; @@ -78,6 +79,10 @@ pub use x509_cert::name::*; /// guarantees about their well-formedness in the context of an actor or home server. If implemented, /// these trait methods should be preferred over the [Constrained] trait method. /// +/// 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. +/// /// [Constrained] does not guarantee that a validated type will always be *correct* in the context /// it is in. /// @@ -87,35 +92,7 @@ pub use x509_cert::name::*; /// the system. However, this makes no implications about "123" being the correct password for a /// given user account. pub trait Constrained { - fn validate(&self) -> Result<(), ConstraintError>; -} - -/// Types implementing [ActorConstrained] act just like types implementing [Constrained], with the -/// additional constraint that they must be well-formed in the context of an actor. -/// -/// For example: While [Constrained] might validate that an IdCert is well-formed, [ActorConstrained] -/// would also check that the IdCert is not a CA certificate, among other things. -/// -/// ## Implementing [ActorConstrained] -/// -/// As [ActorConstrained] is supposed to offer a super-set of the guarantees offered by [Constrained], -/// it is recommended to call [Constrained::validate] in the implementation of [ActorConstrained::validate_actor]. -pub trait ActorConstrained: Constrained { - fn validate_actor(&self) -> Result<(), ConstraintError>; -} - -/// Types implementing [HomeServerConstrained] act just like types implementing [Constrained], with the -/// additional constraint that they must be well-formed in the context of an actor. -/// -/// For example: While [Constrained] might validate that an IdCert is well-formed, [HomeServerConstrained] -/// would also check that the IdCert is a CA certificate, among other things. -/// -/// ## Implementing [HomeServerConstrained] -/// -/// As [HomeServerConstrained] is supposed to offer a super-set of the guarantees offered by [Constrained], -/// it is recommended to call [Constrained::validate] in the implementation of [HomeServerConstrained::validate_actor]. -pub trait HomeServerConstrained: Constrained { - fn validate_home_server(&self) -> Result<(), ConstraintError>; + fn validate(&self, target: Option) -> Result<(), ConstraintError>; } #[cfg(test)] diff --git a/src/types/entities/challenge_string.rs b/src/types/entities/challenge_string.rs index a789297..73f4e94 100644 --- a/src/types/entities/challenge_string.rs +++ b/src/types/entities/challenge_string.rs @@ -8,6 +8,7 @@ use der::Length; use ser_der::asn1::Ia5String; use serde::{Deserialize, Serialize}; +use crate::certs::Target; use crate::errors::base::ConstraintError; use crate::errors::composite::ConversionError; use crate::key::PrivateKey; @@ -61,7 +62,10 @@ impl DerefMut for Challenge { } impl Constrained for Challenge { - fn validate(&self) -> Result<(), crate::errors::base::ConstraintError> { + fn validate( + &self, + _target: Option, + ) -> Result<(), crate::errors::base::ConstraintError> { if self.challenge.len() < Length::new(32) { return Err(ConstraintError::OutOfBounds { lower: 32, diff --git a/tests/idcert.rs b/tests/idcert.rs index b6e9416..f6a91c4 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -98,10 +98,7 @@ fn test_create_ca_cert() { println!(); let csr = polyproto::certs::idcsr::IdCsr::new( - &RdnSequence::from_str( - "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", - ) - .unwrap(), + &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), &priv_key, &Capabilities::default_home_server(), Some(Target::HomeServer), @@ -111,10 +108,7 @@ fn test_create_ca_cert() { csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", - ) - .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(), @@ -145,11 +139,11 @@ fn mismatched_dcs_in_csr_and_cert() { ) .unwrap(), &priv_key, - &Capabilities::default_home_server(), + &Capabilities::default_actor(), Some(Target::Actor), ) .unwrap(); - let cert = IdCert::from_ca_csr( + let cert = IdCert::from_actor_csr( csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), From e7f79fb2e38d3d8fde7b1264b08bb87cdae37349 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 22:12:11 +0200 Subject: [PATCH 069/215] 0.9.0-alpha.6 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2617897..2232269 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.5" +version = "0.9.0-alpha.6" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From 59e2f5791dc8daee8909e1de92ccfc875618c437 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 22:16:05 +0200 Subject: [PATCH 070/215] mark TODOs as done --- src/certs/idcsr.rs | 1 - tests/idcsr.rs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index dc44633..b5e48c9 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -79,7 +79,6 @@ impl> IdCsr { } /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. - // PRETTYFYME: Could be a trait along with to_der, from_pem, to_pem pub fn from_der(bytes: &[u8], target: Option) -> Result { let csr = IdCsr::try_from(CertReq::from_der(bytes)?)?; csr.validate(target)?; diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 255c160..24c09a3 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -29,8 +29,6 @@ use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; use x509_cert::Certificate; -// TODO: Add test where CSR DC and Cert DC are different(?) - /// 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. From 6a7a4b89af152c0c2a74f9e0b9fba486e822ba27 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 23:36:04 +0200 Subject: [PATCH 071/215] Normalize some error messages --- src/constraints/capabilities.rs | 11 +++++++---- src/constraints/certs.rs | 16 ++++++++++------ src/constraints/name.rs | 10 ++++++---- src/errors/mod.rs | 14 ++++++++++---- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/constraints/capabilities.rs b/src/constraints/capabilities.rs index f8f9c7b..2031a40 100644 --- a/src/constraints/capabilities.rs +++ b/src/constraints/capabilities.rs @@ -2,6 +2,8 @@ // 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 { @@ -42,7 +44,7 @@ impl Constrained for Capabilities { // does not matter. if !is_ca && !can_sign && !can_commit_content { return Err(ConstraintError::Malformed(Some( - "Actors require signing capabilities, none found".to_string(), + ERR_MSG_ACTOR_MISSING_SIGNING_CAPS.to_string(), ))); } @@ -62,9 +64,10 @@ impl Constrained for Capabilities { ))); } if !key_cert_sign { - return Err(ConstraintError::Malformed(Some( - "CA must have KeyCertSign capability".to_string(), - ))); + return Err(ConstraintError::Malformed(Some(format!( + "{} Missing capability \"KeyCertSign\"", + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR + )))); } } diff --git a/src/constraints/certs.rs b/src/constraints/certs.rs index 4df5cc7..1c4520a 100644 --- a/src/constraints/certs.rs +++ b/src/constraints/certs.rs @@ -2,6 +2,10 @@ // 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_CANNOT_BE_CA, ERR_MSG_HOME_SERVER_MISSING_CA_ATTR, ERR_MSG_SIGNATURE_MISMATCH, +}; + use super::*; impl> Constrained for IdCsrInner { @@ -13,14 +17,14 @@ impl> Constrained for IdCsrInner { Target::Actor => { if self.capabilities.basic_constraints.ca { return Err(ConstraintError::Malformed(Some( - "Actor CSR must not be a CA".to_string(), + ERR_MSG_ACTOR_CANNOT_BE_CA.to_string(), ))); } } Target::HomeServer => { if !self.capabilities.basic_constraints.ca { return Err(ConstraintError::Malformed(Some( - "Home server CSR must have the CA capability set to true".to_string(), + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR.to_string(), ))); } } @@ -41,7 +45,7 @@ impl> Constrained for IdCsr { } ) { Ok(_) => (), - Err(_) => return Err(ConstraintError::Malformed(Some("Provided signature does not match computed signature".to_string()))) + Err(_) => return Err(ConstraintError::Malformed(Some(ERR_MSG_SIGNATURE_MISMATCH.to_string()))) }; Ok(()) } @@ -63,7 +67,7 @@ impl> Constrained for IdCert { ) { Ok(_) => Ok(()), Err(_) => Err(ConstraintError::Malformed(Some( - "Provided signature does not match computed signature".to_string(), + ERR_MSG_SIGNATURE_MISMATCH.to_string(), ))), } } @@ -87,14 +91,14 @@ impl> Constrained for IdCertTbs { Target::Actor => { if self.capabilities.basic_constraints.ca { return Err(ConstraintError::Malformed(Some( - "Actor cert must not be a CA".to_string(), + ERR_MSG_ACTOR_CANNOT_BE_CA.to_string(), ))); } } Target::HomeServer => { if !self.capabilities.basic_constraints.ca { return Err(ConstraintError::Malformed(Some( - "Home server cert must have the CA capability set to true".to_string(), + ERR_MSG_HOME_SERVER_MISSING_CA_ATTR.to_string(), ))); } } diff --git a/src/constraints/name.rs b/src/constraints/name.rs index eba66c3..8f49ddd 100644 --- a/src/constraints/name.rs +++ b/src/constraints/name.rs @@ -2,6 +2,8 @@ // 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 super::*; impl Constrained for Name { @@ -44,7 +46,7 @@ impl Constrained for Name { lower: 1, upper: 1, actual: num_cn.to_string(), - reason: "Distinguished Names must include exactly one common name attribute.".to_string() + reason: "Distinguished Names must not contain more than one Common Name field".to_string() }); } } @@ -82,7 +84,7 @@ impl Constrained for Name { lower: 1, upper: u8::MAX as i32, actual: "0".to_string(), - reason: "Domain Component is missing".to_string(), + reason: "Domain Component is missing in Name component".to_string(), }); } if num_uid > 1 { @@ -148,7 +150,7 @@ fn validate_dc_matches_dc_in_uid( Some(dc) => dc, None => { return Err(ConstraintError::Malformed(Some( - "Domain Components do not equal the domain components in the UID".to_string(), + ERR_MSG_DC_UID_MISMATCH.to_string(), ))) } }; @@ -159,7 +161,7 @@ fn validate_dc_matches_dc_in_uid( ); if component != &equivalent_dc.to_string() { return Err(ConstraintError::Malformed(Some( - "Domain Components do not equal the domain components in the UID".to_string(), + ERR_MSG_DC_UID_MISMATCH.to_string(), ))); } index = match index.checked_add(1) { diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 8edc686..7c79efa 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -2,11 +2,17 @@ // 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 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!"; /// "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 From 503842398644114f1a538cca408e6fad1fc04e7f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 23:42:13 +0200 Subject: [PATCH 072/215] Add logging to tests --- tests/common/mod.rs | 12 ++++++++++++ tests/idcert.rs | 8 ++++++++ tests/idcsr.rs | 6 ++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/common/mod.rs diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..413d3a7 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,12 @@ +// 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 fn init_logger() { + std::env::set_var("RUST_LOG", "trace"); + env_logger::builder() + .filter_module("crate", log::LevelFilter::Trace) + .is_test(true) + .try_init() + .unwrap_or(()); +} diff --git a/tests/idcert.rs b/tests/idcert.rs index f6a91c4..176ba42 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -4,6 +4,8 @@ #![allow(unused)] +mod common; + use std::str::FromStr; use std::time::Duration; @@ -25,6 +27,8 @@ use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; use x509_cert::Certificate; +use common::init_logger; + /// 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. @@ -45,6 +49,7 @@ use x509_cert::Certificate; #[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()); @@ -91,6 +96,7 @@ fn test_create_actor_cert() { #[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()); @@ -127,6 +133,7 @@ fn test_create_ca_cert() { #[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()); @@ -169,6 +176,7 @@ fn mismatched_dcs_in_csr_and_cert() { #[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 = Ed25519PrivateKey::gen_keypair(&mut csprng); diff --git a/tests/idcsr.rs b/tests/idcsr.rs index 24c09a3..b0d1cb9 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -9,9 +9,12 @@ #![allow(unused)] +mod common; + use std::str::FromStr; use std::time::Duration; +use common::init_logger; use der::asn1::{BitString, Ia5String, Uint, UtcTime}; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; use polyproto::certs::capabilities::{self, Capabilities}; @@ -49,6 +52,7 @@ 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); @@ -70,6 +74,7 @@ fn csr_from_pem() { #[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()); @@ -98,6 +103,7 @@ fn test_create_invalid_actor_csr() { #[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); From 1c5a95da265c9a55ccbc4ec040edd18ac5e0d13e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 23:45:49 +0200 Subject: [PATCH 073/215] Use mod common to clean up code in tests --- tests/common/mod.rs | 153 +++++++++++++++++++++++++++++++++++++ tests/idcert.rs | 164 +--------------------------------------- tests/idcsr.rs | 179 +------------------------------------------- 3 files changed, 155 insertions(+), 341 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 413d3a7..c1f33a4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,6 +2,18 @@ // 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::str::FromStr; + +use der::asn1::BitString; +use ed25519_dalek::ed25519::signature::Signer; +use ed25519_dalek::{Signature as Ed25519DalekSignature, SigningKey, VerifyingKey}; +use polyproto::certs::PublicKeyInfo; +use polyproto::errors::composite::ConversionError; +use polyproto::key::{PrivateKey, PublicKey}; +use polyproto::signature::Signature; +use rand::rngs::OsRng; +use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; + pub fn init_logger() { std::env::set_var("RUST_LOG", "trace"); env_logger::builder() @@ -10,3 +22,144 @@ pub fn init_logger() { .try_init() .unwrap_or(()); } + +#[derive(Debug, PartialEq, Eq, Clone)] +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. +impl Signature for Ed25519Signature { + // We define the signature type from the ed25519-dalek crate as the associated type. + type Signature = Ed25519DalekSignature; + + // This is straightforward: we return a reference to the signature. + fn as_signature(&self) -> &Self::Signature { + &self.signature + } + + // The algorithm identifier for a given signature implementation is constant. We just need + // to define it here. + fn algorithm_identifier() -> AlgorithmIdentifierOwned { + AlgorithmIdentifierOwned { + // This is the OID for Ed25519. It is defined in the IANA registry. + oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), + // For this example, we don't need or want any parameters. + parameters: None, + } + } + + fn from_bytes(signature: &[u8]) -> Self { + let mut signature_vec = signature.to_vec(); + signature_vec.resize(64, 0); + let signature_array: [u8; 64] = { + let mut array = [0; 64]; + array.copy_from_slice(&signature_vec[..]); + array + }; + Self { + signature: Ed25519DalekSignature::from_bytes(&signature_array), + algorithm: Self::algorithm_identifier(), + } + } +} + +// The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement +// it for our signature type. +impl SignatureBitStringEncoding for Ed25519Signature { + fn to_bitstring(&self) -> der::Result { + BitString::from_bytes(&self.as_signature().to_bytes()) + } +} + +// Next, we implement the key traits. We start by defining the private key type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Ed25519PrivateKey { + // Defined below + pub(crate) public_key: Ed25519PublicKey, + // The private key from the ed25519-dalek crate + pub(crate) key: SigningKey, +} + +impl PrivateKey for Ed25519PrivateKey { + type PublicKey = Ed25519PublicKey; + + // Return a reference to the public key + fn pubkey(&self) -> &Self::PublicKey { + &self.public_key + } + + // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can + // harness all of its functionality, such as the `sign` method. + fn sign(&self, data: &[u8]) -> Ed25519Signature { + let signature = self.key.sign(data); + Ed25519Signature { + signature, + algorithm: self.algorithm_identifier(), + } + } +} + +impl Ed25519PrivateKey { + // Let's also define a handy method to generate a key pair. + pub fn gen_keypair(csprng: &mut OsRng) -> Self { + let key = SigningKey::generate(csprng); + let public_key = Ed25519PublicKey { + key: key.verifying_key(), + }; + Self { public_key, key } + } +} + +// Same thing as above for the public key type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Ed25519PublicKey { + // The public key type from the ed25519-dalek crate + pub(crate) key: VerifyingKey, +} + +impl PublicKey for Ed25519PublicKey { + // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. + // This method is used to mitigate weak key forgery. + fn verify_signature( + &self, + signature: &Ed25519Signature, + data: &[u8], + ) -> Result<(), polyproto::errors::composite::PublicKeyError> { + match self.key.verify_strict(data, signature.as_signature()) { + Ok(_) => Ok(()), + Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), + } + } + + // Returns the public key info. Public key info is used to encode the public key in a + // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 + // standard, and thus includes the information needed to encode the public key in a certificate + // or a CSR. + fn public_key_info(&self) -> PublicKeyInfo { + PublicKeyInfo { + algorithm: Ed25519Signature::algorithm_identifier(), + public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), + } + } + + fn try_from_public_key_info(public_key_info: PublicKeyInfo) -> Result { + let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); + key_vec.resize(32, 0); + let signature_array: [u8; 32] = { + let mut array = [0; 32]; + array.copy_from_slice(&key_vec[..]); + array + }; + Ok(Self { + key: VerifyingKey::from_bytes(&signature_array).unwrap(), + }) + } +} diff --git a/tests/idcert.rs b/tests/idcert.rs index 176ba42..70a1049 100644 --- a/tests/idcert.rs +++ b/tests/idcert.rs @@ -27,24 +27,7 @@ use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; use x509_cert::Certificate; -use common::init_logger; - -/// 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 -/// ``` +use common::*; #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -213,148 +196,3 @@ fn cert_from_pem() { let cert_from_der = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(cert_from_der, cert) } - -// 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, -} - -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. - type Signature = Ed25519DalekSignature; - - // This is straightforward: we return a reference to the signature. - fn as_signature(&self) -> &Self::Signature { - &self.signature - } - - // The algorithm identifier for a given signature implementation is constant. We just need - // to define it here. - fn algorithm_identifier() -> AlgorithmIdentifierOwned { - AlgorithmIdentifierOwned { - // This is the OID for Ed25519. It is defined in the IANA registry. - oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), - // For this example, we don't need or want any parameters. - parameters: None, - } - } - - fn from_bytes(signature: &[u8]) -> Self { - let mut signature_vec = signature.to_vec(); - signature_vec.resize(64, 0); - let signature_array: [u8; 64] = { - let mut array = [0; 64]; - array.copy_from_slice(&signature_vec[..]); - array - }; - Self { - signature: Ed25519DalekSignature::from_bytes(&signature_array), - algorithm: Self::algorithm_identifier(), - } - } -} - -// The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement -// it for our signature type. -impl SignatureBitStringEncoding for Ed25519Signature { - fn to_bitstring(&self) -> der::Result { - BitString::from_bytes(&self.as_signature().to_bytes()) - } -} - -// Next, we implement the key traits. We start by defining the private key type. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PrivateKey { - // Defined below - public_key: Ed25519PublicKey, - // The private key from the ed25519-dalek crate - key: SigningKey, -} - -impl PrivateKey for Ed25519PrivateKey { - type PublicKey = Ed25519PublicKey; - - // Return a reference to the public key - fn pubkey(&self) -> &Self::PublicKey { - &self.public_key - } - - // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can - // harness all of its functionality, such as the `sign` method. - fn sign(&self, data: &[u8]) -> Ed25519Signature { - let signature = self.key.sign(data); - Ed25519Signature { - signature, - algorithm: self.algorithm_identifier(), - } - } -} - -impl Ed25519PrivateKey { - // Let's also define a handy method to generate a key pair. - pub fn gen_keypair(csprng: &mut OsRng) -> Self { - let key = SigningKey::generate(csprng); - let public_key = Ed25519PublicKey { - key: key.verifying_key(), - }; - Self { public_key, key } - } -} - -// Same thing as above for the public key type. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PublicKey { - // The public key type from the ed25519-dalek crate - key: VerifyingKey, -} - -impl PublicKey for Ed25519PublicKey { - // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. - // This method is used to mitigate weak key forgery. - fn verify_signature( - &self, - signature: &Ed25519Signature, - data: &[u8], - ) -> Result<(), polyproto::errors::composite::PublicKeyError> { - match self.key.verify_strict(data, signature.as_signature()) { - Ok(_) => Ok(()), - Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), - } - } - - // Returns the public key info. Public key info is used to encode the public key in a - // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 - // standard, and thus includes the information needed to encode the public key in a certificate - // or a CSR. - fn public_key_info(&self) -> PublicKeyInfo { - PublicKeyInfo { - algorithm: Ed25519Signature::algorithm_identifier(), - public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), - } - } - - fn try_from_public_key_info(public_key_info: PublicKeyInfo) -> Result { - let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); - key_vec.resize(32, 0); - let signature_array: [u8; 32] = { - let mut array = [0; 32]; - array.copy_from_slice(&key_vec[..]); - array - }; - Ok(Self { - key: VerifyingKey::from_bytes(&signature_array).unwrap(), - }) - } -} diff --git a/tests/idcsr.rs b/tests/idcsr.rs index b0d1cb9..4ca167d 100644 --- a/tests/idcsr.rs +++ b/tests/idcsr.rs @@ -14,7 +14,7 @@ mod common; use std::str::FromStr; use std::time::Duration; -use common::init_logger; +use common::*; use der::asn1::{BitString, Ia5String, Uint, UtcTime}; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; use polyproto::certs::capabilities::{self, Capabilities}; @@ -23,7 +23,6 @@ use polyproto::certs::idcsr::IdCsr; 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; @@ -32,23 +31,6 @@ 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 -/// ``` - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] fn csr_from_pem() { @@ -121,162 +103,3 @@ fn csr_from_der() { let csr_from_der = IdCsr::from_der(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(csr_from_der, csr) } - -// 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, -} - -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. - type Signature = Ed25519DalekSignature; - - // This is straightforward: we return a reference to the signature. - fn as_signature(&self) -> &Self::Signature { - &self.signature - } - - // The algorithm identifier for a given signature implementation is constant. We just need - // to define it here. - fn algorithm_identifier() -> AlgorithmIdentifierOwned { - AlgorithmIdentifierOwned { - // This is the OID for Ed25519. It is defined in the IANA registry. - oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), - // For this example, we don't need or want any parameters. - parameters: None, - } - } - - #[cfg(not(tarpaulin_include))] - fn from_bytes(signature: &[u8]) -> Self { - let mut signature_vec = signature.to_vec(); - signature_vec.resize(64, 0); - let signature_array: [u8; 64] = { - let mut array = [0; 64]; - array.copy_from_slice(&signature_vec[..]); - array - }; - Self { - signature: Ed25519DalekSignature::from_bytes(&signature_array), - algorithm: Self::algorithm_identifier(), - } - } -} - -// The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement -// it for our signature type. -impl SignatureBitStringEncoding for Ed25519Signature { - fn to_bitstring(&self) -> der::Result { - let unused_bits: u8 = (self.as_signature().to_bytes().len() % 8) - .try_into() - .unwrap(); - BitString::new(unused_bits, self.as_signature().to_bytes().to_vec()) - } -} - -// Next, we implement the key traits. We start by defining the private key type. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PrivateKey { - // Defined below - public_key: Ed25519PublicKey, - // The private key from the ed25519-dalek crate - key: SigningKey, -} - -impl PrivateKey for Ed25519PrivateKey { - type PublicKey = Ed25519PublicKey; - - // Return a reference to the public key - fn pubkey(&self) -> &Self::PublicKey { - &self.public_key - } - - // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can - // harness all of its functionality, such as the `sign` method. - fn sign(&self, data: &[u8]) -> Ed25519Signature { - let signature = self.key.sign(data); - Ed25519Signature { - signature, - algorithm: self.algorithm_identifier(), - } - } -} - -impl Ed25519PrivateKey { - // Let's also define a handy method to generate a key pair. - pub fn gen_keypair(csprng: &mut OsRng) -> Self { - let key = SigningKey::generate(csprng); - let public_key = Ed25519PublicKey { - key: key.verifying_key(), - }; - Self { public_key, key } - } -} - -// Same thing as above for the public key type. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PublicKey { - // The public key type from the ed25519-dalek crate - key: VerifyingKey, -} - -impl PublicKey for Ed25519PublicKey { - // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. - // This method is used to mitigate weak key forgery. - #[cfg(not(tarpaulin_include))] - fn verify_signature( - &self, - signature: &Ed25519Signature, - data: &[u8], - ) -> Result<(), polyproto::errors::composite::PublicKeyError> { - match self.key.verify_strict(data, signature.as_signature()) { - Ok(_) => Ok(()), - Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), - } - } - - // Returns the public key info. Public key info is used to encode the public key in a - // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 - // standard, and thus includes the information needed to encode the public key in a certificate - // or a CSR. - fn public_key_info(&self) -> PublicKeyInfo { - PublicKeyInfo { - algorithm: Ed25519Signature::algorithm_identifier(), - public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), - } - } - - #[cfg(not(tarpaulin_include))] - fn try_from_public_key_info( - public_key_info: PublicKeyInfo, - ) -> std::result::Result { - let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); - key_vec.resize(32, 0); - let signature_array: [u8; 32] = { - let mut array = [0; 32]; - array.copy_from_slice(&key_vec[..]); - array - }; - match VerifyingKey::from_bytes(&signature_array) { - Ok(key) => Ok(Ed25519PublicKey { key }), - Err(e) => Err(polyproto::errors::composite::ConversionError::InvalidInput( - polyproto::errors::base::InvalidInput::Malformed(format!( - "Could not convert public key: {}", - e - )), - )), - } - } -} From d108fdf70c46362983b9cdae4594088062c52191 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 19 May 2024 23:56:24 +0200 Subject: [PATCH 074/215] Start sprinkling in some logs --- src/certs/capabilities/basic_constraints.rs | 61 ++++++++++++++------- src/lib.rs | 4 +- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/certs/capabilities/basic_constraints.rs b/src/certs/capabilities/basic_constraints.rs index 2edb532..d627cb5 100644 --- a/src/certs/capabilities/basic_constraints.rs +++ b/src/certs/capabilities/basic_constraints.rs @@ -6,6 +6,7 @@ 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; @@ -166,20 +167,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 +197,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 +227,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 +257,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 +269,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 +287,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/lib.rs b/src/lib.rs index ac0c44a..28bf402 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,7 +98,9 @@ pub trait Constrained { #[cfg(test)] pub(crate) mod testing_utils { pub(crate) fn init_logger() { - std::env::set_var("RUST_LOG", "trace"); + 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() From d3781ed29a6f99d0c01d4259f174f14b0909490c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 01:23:43 +0200 Subject: [PATCH 075/215] Normalize error messages --- src/certs/idcert.rs | 14 ++++++++++++-- src/errors/mod.rs | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 628e40a..1e09f49 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -5,12 +5,14 @@ use der::asn1::Uint; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; +use log::warn; 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::ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; use crate::Constrained; @@ -55,8 +57,12 @@ impl> IdCert { // IdCsr gets validated in IdCertTbs::from_..._csr let signature_algorithm = signing_key.algorithm_identifier(); if !equal_domain_components(&id_csr.inner_csr.subject, &issuer) { + warn!( + "{}\nIssuer: {}\nSubject: {}", + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, issuer, id_csr.inner_csr.subject + ); return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - "Domain components of the issuer and the subject do not match".to_string(), + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT.to_string(), ))); } let id_cert_tbs = @@ -87,9 +93,13 @@ impl> IdCert { let signature_algorithm = signing_key.algorithm_identifier(); issuer.validate(Some(Target::Actor))?; if !equal_domain_components(&id_csr.inner_csr.subject, &issuer) { + warn!( + "{}\nIssuer: {}\nSubject: {}", + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, issuer, id_csr.inner_csr.subject + ); return Err(ConversionError::InvalidInput( crate::errors::base::InvalidInput::Malformed( - "Domain components of the issuer and the subject do not match".to_string(), + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT.to_string(), ), )); } diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 7c79efa..8fc0eee 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -12,6 +12,8 @@ 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!"; /// "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 From d7a3e82b7adc406253a4831b4d23910115cd6f68 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 01:24:09 +0200 Subject: [PATCH 076/215] Remove redundant checks --- src/certs/idcerttbs.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 3ce54de..8a3e316 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -12,7 +12,6 @@ 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::key::PublicKey; use crate::signature::Signature; @@ -76,11 +75,6 @@ impl> IdCertTbs { 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(Some(Target::Actor))?; issuer.validate(Some(Target::Actor))?; // Verify if signature of IdCsr matches contents @@ -113,12 +107,8 @@ impl> IdCertTbs { 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(Some(Target::HomeServer))?; + issuer.validate(Some(Target::HomeServer))?; // Verify if signature of IdCsr matches contents id_csr.inner_csr.subject_public_key.verify_signature( &id_csr.signature, From 574ce2735489a7c033771303d7f2937435f9b832 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 12:23:22 +0200 Subject: [PATCH 077/215] API changes to from_der, added information about certificate safety --- src/certs/idcert.rs | 18 +++++++++++++++--- src/certs/idcerttbs.rs | 17 ++++++++++++++--- src/certs/idcsr.rs | 16 +++++++++++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 1e09f49..9f933c2 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -47,6 +47,9 @@ impl> IdCert { /// the [BasicConstraints] "ca" flag set to `false`. /// /// See [IdCert::from_actor_csr()] when trying to create a new actor certificate. + /// + /// The resulting `IdCert` 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, signing_key: &impl PrivateKey, @@ -82,6 +85,9 @@ impl> IdCert { /// the [BasicConstraints] "ca" flag set to `false`. /// /// See [IdCert::from_ca_csr()] when trying to create a new ca certificate. + /// + /// The resulting `IdCert` 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, signing_key: &impl PrivateKey, @@ -120,6 +126,8 @@ impl> IdCert { } /// Create an IdCsr from a byte slice containing a DER encoded X.509 Certificate. + /// The resulting `IdCert` 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(value: &[u8], target: Option) -> Result { let cert = IdCert::try_from(Certificate::from_der(value)?)?; cert.validate(target)?; @@ -131,7 +139,9 @@ impl> IdCert { Ok(Certificate::try_from(self)?.to_der()?) } - /// Create an IdCsr from a byte slice containing a PEM encoded X.509 Certificate. + /// Create an [IdCert] from a byte slice containing a PEM encoded X.509 Certificate. + /// The resulting `IdCert` 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_pem(pem: &str, target: Option) -> Result { let cert = IdCert::try_from(Certificate::from_pem(pem)?)?; cert.validate(target)?; @@ -167,7 +177,10 @@ 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_bytes(value.signature.raw_bytes()); @@ -175,7 +188,6 @@ impl> TryFrom for IdCert { id_cert_tbs, signature, }; - cert.validate(None)?; Ok(cert) } } diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 8a3e316..223d776 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -68,6 +68,9 @@ 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. + /// + /// The resulting `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, + /// for the usage context of an actor certificate. pub(crate) fn from_actor_csr( id_csr: IdCsr, serial_number: Uint, @@ -100,6 +103,9 @@ impl> IdCertTbs { /// the [BasicConstraints] "ca" flag set to `false`. /// /// See [IdCertTbs::from_actor_csr()] when trying to create a new actor certificate. + /// + /// The resulting `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, + /// for the usage context of a home server certificate. pub(crate) fn from_ca_csr( id_csr: IdCsr, serial_number: Uint, @@ -131,10 +137,12 @@ 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], target: Target) -> Result { + /// Create an IdCsr 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::try_from(TbsCertificate::from_der(bytes)?)?; - cert.validate(Some(target))?; + cert.validate(target)?; Ok(cert) } } @@ -144,6 +152,9 @@ 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(None)?; diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index b5e48c9..62053ad 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -59,6 +59,9 @@ impl> IdCsr { /// sign the CSR. /// - **subject_unique_id**: [Uint], subject (actor) session ID. MUST NOT exceed 32 characters /// in length. + /// + /// 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, @@ -140,6 +143,9 @@ 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. pub fn new( subject: &Name, public_key: &P, @@ -161,7 +167,9 @@ impl> IdCsrInner { }) } - /// Create an IdCsrInner from a byte slice containing a DER encoded PKCS #10 CSR. + /// 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)?; @@ -177,6 +185,9 @@ 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)?, @@ -189,6 +200,9 @@ 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(None)?; From 37e0f9be6fe1778135b145d61c531b2bc93f0e81 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 12:34:11 +0200 Subject: [PATCH 078/215] Add `from_{"der", "pem"}_unchecked` methods for IdCsr, IdCsrInner, IdCertTbs, IdCert --- src/certs/idcert.rs | 22 +++++++++++++++++++--- src/certs/idcerttbs.rs | 12 ++++++++++-- src/certs/idcsr.rs | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 9f933c2..cb8ae1f 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -125,15 +125,23 @@ impl> IdCert { Ok(cert) } - /// Create an IdCsr from a byte slice containing a DER encoded X.509 Certificate. + /// Create an [IdCert] from a byte slice containing a DER encoded X.509 Certificate. /// The resulting `IdCert` 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(value: &[u8], target: Option) -> Result { - let cert = IdCert::try_from(Certificate::from_der(value)?)?; + let cert = IdCert::from_der_unchecked(value)?; cert.validate(target)?; 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) + } + /// Encode this type as DER, returning a byte vector. pub fn to_der(self) -> Result, ConversionError> { Ok(Certificate::try_from(self)?.to_der()?) @@ -143,11 +151,19 @@ impl> IdCert { /// The resulting `IdCert` 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_pem(pem: &str, target: Option) -> Result { - let cert = IdCert::try_from(Certificate::from_pem(pem)?)?; + let cert = IdCert::from_pem_unchecked(pem)?; cert.validate(target)?; 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 + /// the [Constrained] trait 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)?) diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 223d776..939d587 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -137,14 +137,22 @@ impl> IdCertTbs { Ok(TbsCertificate::try_from(self)?.to_der()?) } - /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. The resulting + /// 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::try_from(TbsCertificate::from_der(bytes)?)?; + 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) + } } impl> TryFrom> diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 62053ad..06d5dd9 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -81,25 +81,45 @@ impl> IdCsr { }) } - /// Create an IdCsr from a byte slice containing a DER encoded PKCS #10 CSR. + /// 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::try_from(CertReq::from_der(bytes)?)?; + let csr = IdCsr::from_der_unchecked(bytes)?; csr.validate(target)?; Ok(csr) } + /// 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. + /// 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::try_from(CertReq::from_pem(pem)?)?; + 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)?) @@ -176,6 +196,14 @@ impl> IdCsrInner { Ok(csr_inner) } + /// 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. pub fn to_der(self) -> Result, ConversionError> { Ok(CertReqInfo::try_from(self)?.to_der()?) From cb3e84e2ecbf14b7233a5b68055da701e4fbf716 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 12:54:56 +0200 Subject: [PATCH 079/215] Improve efficiency by removing double and triple validation of the same thing --- src/certs/idcert.rs | 49 ++++++++++++++-------------------------- src/certs/idcerttbs.rs | 30 +++++++++--------------- src/certs/idcsr.rs | 27 ++++++++++++---------- src/constraints/certs.rs | 15 ++++++++---- 4 files changed, 54 insertions(+), 67 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index cb8ae1f..88f651b 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -5,21 +5,18 @@ use der::asn1::Uint; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; -use log::warn; 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::ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; use crate::Constrained; use super::idcerttbs::IdCertTbs; use super::idcsr::IdCsr; -use super::{equal_domain_components, Target}; +use super::Target; /// A signed polyproto ID-Cert, consisting of the actual certificate, the CA-generated signature and /// metadata about that signature. @@ -57,19 +54,17 @@ 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) { - warn!( - "{}\nIssuer: {}\nSubject: {}", - ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, issuer, id_csr.inner_csr.subject - ); - return Err(ConversionError::InvalidInput(InvalidInput::Malformed( - ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT.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, @@ -95,27 +90,17 @@ impl> IdCert { issuer: Name, validity: Validity, ) -> Result { - // IdCsr gets validated in IdCertTbs::from_..._csr let signature_algorithm = signing_key.algorithm_identifier(); - issuer.validate(Some(Target::Actor))?; - if !equal_domain_components(&id_csr.inner_csr.subject, &issuer) { - warn!( - "{}\nIssuer: {}\nSubject: {}", - ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, issuer, id_csr.inner_csr.subject - ); - return Err(ConversionError::InvalidInput( - crate::errors::base::InvalidInput::Malformed( - ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT.to_string(), - ), - )); - } - let id_cert_tbs = IdCertTbs::from_actor_csr( - id_csr, + 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, diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 939d587..7d93ad9 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -58,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 { @@ -71,7 +71,7 @@ impl> IdCertTbs { /// /// The resulting `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, /// for the usage context of an actor certificate. - pub(crate) fn from_actor_csr( + pub fn from_actor_csr( id_csr: IdCsr, serial_number: Uint, signature_algorithm: AlgorithmIdentifierOwned, @@ -79,13 +79,7 @@ impl> IdCertTbs { validity: Validity, ) -> Result { id_csr.validate(Some(Target::Actor))?; - issuer.validate(Some(Target::Actor))?; - // 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 { + let cert_tbs = IdCertTbs { serial_number, signature_algorithm, issuer, @@ -94,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,7 +102,7 @@ impl> IdCertTbs { /// /// The resulting `IdCertTbs` is guaranteed to be well-formed and up to polyproto specification, /// for the usage context of a home server certificate. - pub(crate) fn from_ca_csr( + pub fn from_ca_csr( id_csr: IdCsr, serial_number: Uint, signature_algorithm: AlgorithmIdentifierOwned, @@ -114,13 +110,7 @@ impl> IdCertTbs { validity: Validity, ) -> Result { id_csr.validate(Some(Target::HomeServer))?; - issuer.validate(Some(Target::HomeServer))?; - // 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 { + let cert_tbs = IdCertTbs { serial_number, signature_algorithm, issuer, @@ -129,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. diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 06d5dd9..f2338c3 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -68,17 +68,22 @@ impl> IdCsr { capabilities: &Capabilities, target: Option, ) -> Result, ConversionError> { - subject.validate(target)?; - let inner_csr = - IdCsrInner::::new(subject, signing_key.pubkey(), capabilities, target)?; + 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, - }) + }; + id_csr.validate(target)?; + Ok(id_csr) } /// Create an [IdCsr] from a byte slice containing a DER encoded PKCS #10 CSR. @@ -172,19 +177,17 @@ impl> IdCsrInner { capabilities: &Capabilities, target: Option, ) -> Result, ConversionError> { - subject.validate(target)?; - capabilities.validate(target)?; - 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. diff --git a/src/constraints/certs.rs b/src/constraints/certs.rs index 1c4520a..eb44fcc 100644 --- a/src/constraints/certs.rs +++ b/src/constraints/certs.rs @@ -2,8 +2,11 @@ // 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_HOME_SERVER_MISSING_CA_ATTR, ERR_MSG_SIGNATURE_MISMATCH, + 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::*; @@ -79,11 +82,15 @@ impl> Constrained for IdCertTbs { self.issuer.validate(target)?; self.subject.validate(target)?; match equal_domain_components(&self.issuer, &self.subject) { - true => (), + 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( - "Domain components of issuer and subject are not equal".to_string(), - ))) + ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT.to_string(), + ))); } } if let Some(target) = target { From 70b6e014e7c2f73ef103ffd0ba31dd354ac491d9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 13:07:49 +0200 Subject: [PATCH 080/215] Publish v0.9.0-alpha.7 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2232269..d0d89ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.6" +version = "0.9.0-alpha.7" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From af57fe80083b4889e6cd5dc7b415941da93f9626 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 16:30:56 +0200 Subject: [PATCH 081/215] move ChallengePayload to entities dir --- src/types/entities/challenge_string.rs | 19 +++++++++++++++++++ src/types/schemas/authentication.rs | 22 +--------------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/types/entities/challenge_string.rs b/src/types/entities/challenge_string.rs index 73f4e94..0e988ff 100644 --- a/src/types/entities/challenge_string.rs +++ b/src/types/entities/challenge_string.rs @@ -95,3 +95,22 @@ pub struct CompletedChallenge { /// The signature of the challenge. pub signature: S, } + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +/// Completed challenge payload. +pub struct ChallengePayload { + /// The challenge string. + pub challenge: String, + /// The signature of the challenge. + pub signature: String, +} + +impl From> for ChallengePayload { + fn from(value: CompletedChallenge) -> Self { + Self { + challenge: value.challenge.to_string(), + signature: value.signature.to_string(), + } + } +} diff --git a/src/types/schemas/authentication.rs b/src/types/schemas/authentication.rs index 38c2970..71b3b4f 100644 --- a/src/types/schemas/authentication.rs +++ b/src/types/schemas/authentication.rs @@ -6,7 +6,7 @@ use crate::certs::idcert::IdCert; use crate::errors::composite::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; -use crate::types::entities::{Challenge, CompletedChallenge}; +use crate::types::entities::{Challenge, ChallengePayload}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] @@ -60,26 +60,6 @@ impl TryFrom for Challenge { } } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Completed challenge payload. -// TODO: Move this to /types/entities or another, more appropriate module. -pub struct ChallengePayload { - /// The challenge string. - pub challenge: String, - /// The signature of the challenge. - pub signature: String, -} - -impl From> for ChallengePayload { - fn from(value: CompletedChallenge) -> Self { - Self { - challenge: value.challenge.to_string(), - signature: value.signature.to_string(), - } - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] /// Identify payload to log a session in. From 9a995a756551c5d39759351be3d568ff7ffd387e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 17:08:22 +0200 Subject: [PATCH 082/215] move tests to subfolder --- tests/{ => certificates}/idcert.rs | 4 +--- tests/{ => certificates}/idcsr.rs | 4 +--- tests/certificates/mod.rs | 8 ++++++++ tests/mod.rs | 8 ++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) rename tests/{ => certificates}/idcert.rs (99%) rename tests/{ => certificates}/idcsr.rs (99%) create mode 100644 tests/certificates/mod.rs create mode 100644 tests/mod.rs diff --git a/tests/idcert.rs b/tests/certificates/idcert.rs similarity index 99% rename from tests/idcert.rs rename to tests/certificates/idcert.rs index 70a1049..5538c18 100644 --- a/tests/idcert.rs +++ b/tests/certificates/idcert.rs @@ -4,8 +4,6 @@ #![allow(unused)] -mod common; - use std::str::FromStr; use std::time::Duration; @@ -27,7 +25,7 @@ use x509_cert::request::CertReq; use x509_cert::time::{Time, Validity}; use x509_cert::Certificate; -use common::*; +use crate::common::*; #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] diff --git a/tests/idcsr.rs b/tests/certificates/idcsr.rs similarity index 99% rename from tests/idcsr.rs rename to tests/certificates/idcsr.rs index 4ca167d..22787fa 100644 --- a/tests/idcsr.rs +++ b/tests/certificates/idcsr.rs @@ -9,12 +9,10 @@ #![allow(unused)] -mod common; - use std::str::FromStr; use std::time::Duration; -use common::*; +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}; diff --git a/tests/certificates/mod.rs b/tests/certificates/mod.rs new file mode 100644 index 0000000..f6a9277 --- /dev/null +++ b/tests/certificates/mod.rs @@ -0,0 +1,8 @@ +// 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/. + +mod idcert; +mod idcsr; diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 0000000..1c89d37 --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,8 @@ +// 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/. + +pub(crate) mod certificates; +pub(crate) mod common; From 247a3c616559beb4c8a7f54cd0fe0ca73220e8f5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 17:43:35 +0200 Subject: [PATCH 083/215] Delete API dir to start fresh --- src/api/events/mod.rs | 3 - src/api/identity/mod.rs | 52 --------- src/api/mod.rs | 4 - src/types/entities/challenge_string.rs | 116 ------------------- src/types/entities/mod.rs | 7 -- src/types/mod.rs | 6 - src/types/schemas/authentication.rs | 85 -------------- src/types/schemas/mod.rs | 5 - {src/api/authentication => tests/api}/mod.rs | 0 tests/certificates/mod.rs | 2 - tests/mod.rs | 2 - 11 files changed, 282 deletions(-) delete mode 100644 src/api/events/mod.rs delete mode 100644 src/api/identity/mod.rs delete mode 100644 src/types/entities/challenge_string.rs delete mode 100644 src/types/entities/mod.rs delete mode 100644 src/types/mod.rs delete mode 100644 src/types/schemas/authentication.rs delete mode 100644 src/types/schemas/mod.rs rename {src/api/authentication => tests/api}/mod.rs (100%) diff --git a/src/api/events/mod.rs b/src/api/events/mod.rs deleted file mode 100644 index 7be716e..0000000 --- a/src/api/events/mod.rs +++ /dev/null @@ -1,3 +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 https://mozilla.org/MPL/2.0/. diff --git a/src/api/identity/mod.rs b/src/api/identity/mod.rs deleted file mode 100644 index 84897d0..0000000 --- a/src/api/identity/mod.rs +++ /dev/null @@ -1,52 +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 https://mozilla.org/MPL/2.0/. - -use reqwest::Method; - -use crate::certs::idcert::IdCert; -use crate::key::PublicKey; -use crate::signature::Signature; -use crate::types::entities::Challenge; - -use super::{HttpClient, HttpResult}; - -impl HttpClient { - /// Request a challenge string. - /// - /// **GET** *{base_url}*/.p2/core/v1/challenge - pub async fn get_challenge(&self, base_url: &str) -> HttpResult { - let response = self - .request( - Method::GET, - &format!("{}/.p2/core/v1/challenge", base_url), - None, - ) - .await; - - Self::handle_response(response).await - } - - /// Rotate the server's identity key. Returns the new server [IdCert]. - /// - /// **PUT** *{base_url}*/.p2/core/v1/key/server - /// - Requires authentication - /// - Requires server administrator privileges - pub async fn rotate_server_identity_key>( - &self, - base_url: &str, - ) -> HttpResult> { - let response = self - .request( - Method::PUT, - &format!("{}/.p2/core/v1/key/server", base_url), - None, - ) - .await?; - - Ok(IdCert::from_pem( - response.text().await?.as_str(), - Some(crate::certs::Target::HomeServer), - )?) - } -} diff --git a/src/api/mod.rs b/src/api/mod.rs index f308635..a3764e7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -7,10 +7,6 @@ use serde_json::from_str; use crate::errors::composite::RequestError; -pub mod authentication; -pub mod events; -pub mod identity; - #[derive(Debug, Default, Clone)] pub struct HttpClient { client: reqwest::Client, diff --git a/src/types/entities/challenge_string.rs b/src/types/entities/challenge_string.rs deleted file mode 100644 index 0e988ff..0000000 --- a/src/types/entities/challenge_string.rs +++ /dev/null @@ -1,116 +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 https://mozilla.org/MPL/2.0/. - -use std::ops::{Deref, DerefMut}; - -use der::Length; -use ser_der::asn1::Ia5String; -use serde::{Deserialize, Serialize}; - -use crate::certs::Target; -use crate::errors::base::ConstraintError; -use crate::errors::composite::ConversionError; -use crate::key::PrivateKey; -use crate::signature::Signature; -use crate::Constrained; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -/// A challenge string, used to prove that an actor possesses a private key, without revealing it. -pub struct Challenge { - pub(crate) challenge: Ia5String, - pub expires: u64, -} - -impl Challenge { - /// Creates a new challenge string. - /// - /// ## Arguments - /// - /// - **challenge**: The challenge string. - /// - **expires**: The UNIX timestamp when the challenge expires. - pub fn new(challenge: &str, expires: u64) -> Result { - let ia5string = der::asn1::Ia5String::new(challenge)?; - Ok(Self { - challenge: ia5string.into(), - expires, - }) - } - - /// Completes the challenge by signing it with the private key. - pub fn complete>(&self, key: &V) -> CompletedChallenge { - let s = key.sign(self.challenge.as_bytes()); - CompletedChallenge { - challenge: self.clone(), - signature: s, - } - } -} - -impl Deref for Challenge { - type Target = Ia5String; - - fn deref(&self) -> &Self::Target { - &self.challenge - } -} - -impl DerefMut for Challenge { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.challenge - } -} - -impl Constrained for Challenge { - fn validate( - &self, - _target: Option, - ) -> Result<(), crate::errors::base::ConstraintError> { - if self.challenge.len() < Length::new(32) { - return Err(ConstraintError::OutOfBounds { - lower: 32, - upper: 256, - actual: self.challenge.len().to_string(), - reason: "Challenge string must be at least 32 characters long".to_string(), - }); - } - - if self.challenge.len() > Length::new(256) { - return Err(ConstraintError::OutOfBounds { - lower: 32, - upper: 256, - actual: self.challenge.len().to_string(), - reason: "Challenge string must be at most 256 characters long".to_string(), - }); - } - - Ok(()) - } -} - -/// A completed challenge, containing the challenge and the signature. -pub struct CompletedChallenge { - /// The challenge. - pub challenge: Challenge, - /// The signature of the challenge. - pub signature: S, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Completed challenge payload. -pub struct ChallengePayload { - /// The challenge string. - pub challenge: String, - /// The signature of the challenge. - pub signature: String, -} - -impl From> for ChallengePayload { - fn from(value: CompletedChallenge) -> Self { - Self { - challenge: value.challenge.to_string(), - signature: value.signature.to_string(), - } - } -} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs deleted file mode 100644 index ed41408..0000000 --- a/src/types/entities/mod.rs +++ /dev/null @@ -1,7 +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 https://mozilla.org/MPL/2.0/. - -pub mod challenge_string; - -pub use challenge_string::*; diff --git a/src/types/mod.rs b/src/types/mod.rs deleted file mode 100644 index 78b9bde..0000000 --- a/src/types/mod.rs +++ /dev/null @@ -1,6 +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 https://mozilla.org/MPL/2.0/. - -pub mod entities; -pub mod schemas; diff --git a/src/types/schemas/authentication.rs b/src/types/schemas/authentication.rs deleted file mode 100644 index 71b3b4f..0000000 --- a/src/types/schemas/authentication.rs +++ /dev/null @@ -1,85 +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 https://mozilla.org/MPL/2.0/. - -use crate::certs::idcert::IdCert; -use crate::errors::composite::ConversionError; -use crate::key::PublicKey; -use crate::signature::Signature; -use crate::types::entities::{Challenge, ChallengePayload}; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Schema for creating a new session. -/// -/// `/p2core/session/trust` -pub struct CreateSessionRequest { - /// The name of the actor that is creating the session. - pub actor_name: String, - /// PEM encoded [IdCsr] - pub csr: String, - /// Optional authentication payload. - pub auth_payload: Option, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Response counterpart of [CreateSessionSchema]. -pub struct CreateSessionResponse { - /// PEM encoded [IdCert] - pub id_cert: String, - /// An authentication (bearer) token. - pub token: String, -} - -impl> TryFrom for IdCert { - type Error = ConversionError; - - fn try_from(value: CreateSessionResponse) -> Result { - Self::from_pem(&value.id_cert, Some(crate::certs::Target::Actor)) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Schema for getting a challenge. Can be converted to [Challenge] using [TryFrom]. -/// -/// `/p2core/challenge` -pub struct GetChallengeResponse { - /// The challenge string. - pub challenge: String, - /// UNIX timestamp when the challenge expires. - pub expires: u64, -} - -impl TryFrom for Challenge { - type Error = ConversionError; - - fn try_from(value: GetChallengeResponse) -> Result { - Challenge::new(&value.challenge, value.expires) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Identify payload to log a session in. -/// -/// `/p2core/session/identify` -pub struct IdentifyRequest { - /// A completed challenge. - pub challenge_signature: ChallengePayload, - /// PEM encoded [IdCert] - pub id_cert: String, - /// Optional authentication payload. - pub auth_payload: Option, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] -/// Response counterpart of [IdentifyRequest]. -pub struct IdentifyResponse { - /// An authentication (bearer) token. - pub token: String, - /// Optional payload from the server. - pub payload: Option, -} diff --git a/src/types/schemas/mod.rs b/src/types/schemas/mod.rs deleted file mode 100644 index b05c34b..0000000 --- a/src/types/schemas/mod.rs +++ /dev/null @@ -1,5 +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 https://mozilla.org/MPL/2.0/. - -pub mod authentication; diff --git a/src/api/authentication/mod.rs b/tests/api/mod.rs similarity index 100% rename from src/api/authentication/mod.rs rename to tests/api/mod.rs diff --git a/tests/certificates/mod.rs b/tests/certificates/mod.rs index f6a9277..cc6fc54 100644 --- a/tests/certificates/mod.rs +++ b/tests/certificates/mod.rs @@ -1,5 +1,3 @@ -// 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/. diff --git a/tests/mod.rs b/tests/mod.rs index 1c89d37..c9ef28a 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,5 +1,3 @@ -// 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/. From 0a1d5f947f19fd34f0a985a1d14ac6947c22aaab Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 20:34:47 +0200 Subject: [PATCH 084/215] Start working on API types --- .vscode/settings.json | 2 +- Cargo.toml | 6 ++--- src/constraints/mod.rs | 2 ++ src/constraints/types.rs | 36 +++++++++++++++++++++++++++ src/errors/composite.rs | 2 +- src/errors/mod.rs | 6 +++++ src/lib.rs | 2 +- src/types/authorization.rs | 10 ++++++++ src/types/mod.rs | 46 +++++++++++++++++++++++++++++++++++ tests/api/mod.rs | 50 ++++++++++++++++++++++++++++++++++++++ tests/mod.rs | 3 +++ 11 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 src/constraints/types.rs create mode 100644 src/types/authorization.rs create mode 100644 src/types/mod.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 182ed5f..9d4cb70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "markiscodecoverage.searchCriteria": ".coverage/lcov*.info", - "rust-analyzer.cargo.features": ["routes"] + "rust-analyzer.cargo.features": ["types", "reqwest"] } diff --git a/Cargo.toml b/Cargo.toml index d0d89ba..5d39517 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,11 @@ rust-version = "1.65.0" crate-type = ["rlib", "cdylib", "staticlib"] [features] -default = ["routes", "reqwest"] +default = ["types"] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] -routes = ["serde"] -reqwest = ["dep:reqwest", "routes"] +types = [] +reqwest = ["dep:reqwest", "types", "serde"] serde = ["dep:serde", "dep:serde_json", "dep:ser_der"] [dependencies] diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs index 3019074..ec1e6dc 100644 --- a/src/constraints/mod.rs +++ b/src/constraints/mod.rs @@ -24,6 +24,8 @@ pub mod capabilities; pub mod certs; pub mod name; pub mod session_id; +#[cfg(feature = "types")] +pub mod types; #[cfg(test)] mod name_constraints { diff --git a/src/constraints/types.rs b/src/constraints/types.rs new file mode 100644 index 0000000..b4177f6 --- /dev/null +++ b/src/constraints/types.rs @@ -0,0 +1,36 @@ +// 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, ERR_MSG_FEDERATION_ID_REGEX}; +use crate::types::authorization::ChallengeString; +use crate::types::FederationId; +use regex::Regex; + +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(()) + } +} + +impl Constrained for FederationId { + fn validate(&self, _target: Option) -> Result<(), ConstraintError> { + let fid_regex = Regex::new(r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)").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/errors/composite.rs b/src/errors/composite.rs index edded0d..5a41946 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -44,7 +44,7 @@ pub enum ConversionError { #[error(transparent)] IdCertError(#[from] PublicKeyError), } -#[cfg(feature = "routes")] +#[cfg(feature = "reqwest")] #[derive(Error, Debug)] pub enum RequestError { #[error(transparent)] diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 8fc0eee..3edf7eb 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -14,6 +14,12 @@ 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!"; +#[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 diff --git a/src/lib.rs b/src/lib.rs index 28bf402..abdb174 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ pub mod errors; pub mod key; /// Generic polyproto signature traits. pub mod signature; -#[cfg(feature = "routes")] +#[cfg(feature = "types")] /// Types used in polyproto and the polyproto HTTP/REST APIs pub mod types; diff --git a/src/types/authorization.rs b/src/types/authorization.rs new file mode 100644 index 0000000..ad00430 --- /dev/null +++ b/src/types/authorization.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/. + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ChallengeString { + pub challenge: String, + pub expires: u64, +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..af138ea --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,46 @@ +// 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 crate::errors::base::ConstraintError; +use crate::Constrained; + +pub mod authorization; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +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 fid = Self { + inner: id.to_string(), + }; + fid.validate(None)?; + Ok(fid) + } +} + +impl std::fmt::Display for FederationId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 7be716e..259a2e1 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -1,3 +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 super::*; +use polyproto::types::authorization::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()); +} diff --git a/tests/mod.rs b/tests/mod.rs index c9ef28a..fba19ce 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -2,5 +2,8 @@ // 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 certificates; pub(crate) mod common; + +use polyproto::Constrained; From f261775c387650c37935185cb074beec5f14ae8a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 22:44:05 +0200 Subject: [PATCH 085/215] Write some tests to confirm FederationId parsing is working --- tests/api/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 259a2e1..1a3f86c 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -49,5 +49,9 @@ fn invalid_federation_id() { 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!(FederationId::new("example@😿.com").is_err()); + assert_eq!( + *FederationId::new("example@com.⾆").unwrap(), + "example@com".to_string() + ); } From 7434ab9b8b3dfd7c4a745d4c40d1da983db91f1c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 22:44:43 +0200 Subject: [PATCH 086/215] Normalize REGEX_FEDERATION_ID --- src/constraints/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/types.rs b/src/constraints/types.rs index b4177f6..e030a36 100644 --- a/src/constraints/types.rs +++ b/src/constraints/types.rs @@ -4,7 +4,7 @@ use crate::errors::{ERR_MSG_CHALLENGE_STRING_LENGTH, ERR_MSG_FEDERATION_ID_REGEX}; use crate::types::authorization::ChallengeString; -use crate::types::FederationId; +use crate::types::{FederationId, REGEX_FEDERATION_ID}; use regex::Regex; use super::*; @@ -25,7 +25,7 @@ impl Constrained for ChallengeString { impl Constrained for FederationId { fn validate(&self, _target: Option) -> Result<(), ConstraintError> { - let fid_regex = Regex::new(r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)").unwrap(); + let fid_regex = Regex::new(REGEX_FEDERATION_ID).unwrap(); match fid_regex.is_match(&self.inner) { true => Ok(()), false => Err(ConstraintError::Malformed(Some( From b58f0f7d105b4a23ae89ba2e84c99283617eed11 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 20 May 2024 22:45:02 +0200 Subject: [PATCH 087/215] Regex for FederationId parsing --- src/types/mod.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index af138ea..b6a1aab 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,11 +4,16 @@ use std::ops::{Deref, DerefMut}; +use regex::Regex; + use crate::errors::base::ConstraintError; +use crate::errors::ERR_MSG_FEDERATION_ID_REGEX; use crate::Constrained; pub mod authorization; +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)] pub struct FederationId { pub(crate) inner: String, @@ -31,11 +36,26 @@ impl DerefMut for FederationId { impl FederationId { /// Validates input, then creates a new `FederationId`. pub fn new(id: &str) -> Result { - let fid = Self { - inner: id.to_string(), + 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 }; - fid.validate(None)?; - Ok(fid) + 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(), + ))) + } } } From 9e2c76cccc3b77f60d9ed81c5bb826d4b1a44cd0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 21 May 2024 22:06:48 +0200 Subject: [PATCH 088/215] Change file structure of types/ dir --- src/constraints/types.rs | 2 +- .../{authorization.rs => challenge_string.rs} | 0 src/types/federation_id.rs | 64 ++++++++++++++++++ src/types/mod.rs | 65 ++----------------- tests/api/mod.rs | 2 +- 5 files changed, 70 insertions(+), 63 deletions(-) rename src/types/{authorization.rs => challenge_string.rs} (100%) create mode 100644 src/types/federation_id.rs diff --git a/src/constraints/types.rs b/src/constraints/types.rs index e030a36..032b9b7 100644 --- a/src/constraints/types.rs +++ b/src/constraints/types.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::errors::{ERR_MSG_CHALLENGE_STRING_LENGTH, ERR_MSG_FEDERATION_ID_REGEX}; -use crate::types::authorization::ChallengeString; +use crate::types::ChallengeString; use crate::types::{FederationId, REGEX_FEDERATION_ID}; use regex::Regex; diff --git a/src/types/authorization.rs b/src/types/challenge_string.rs similarity index 100% rename from src/types/authorization.rs rename to src/types/challenge_string.rs diff --git a/src/types/federation_id.rs b/src/types/federation_id.rs new file mode 100644 index 0000000..bb8cd4f --- /dev/null +++ b/src/types/federation_id.rs @@ -0,0 +1,64 @@ +// 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::base::ConstraintError; +use crate::errors::ERR_MSG_FEDERATION_ID_REGEX; +use crate::Constrained; + +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)] +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 index b6a1aab..ac86f27 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,65 +2,8 @@ // 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}; +pub mod challenge_string; +pub mod federation_id; -use regex::Regex; - -use crate::errors::base::ConstraintError; -use crate::errors::ERR_MSG_FEDERATION_ID_REGEX; -use crate::Constrained; - -pub mod authorization; - -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)] -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) - } -} +pub use challenge_string::*; +pub use federation_id::*; diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 1a3f86c..3f8df27 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use super::*; -use polyproto::types::authorization::ChallengeString; +use polyproto::types::ChallengeString; use polyproto::types::FederationId; #[test] From fa73c6ccf45db6a5e7c5d42a77c3607989175c76 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 21 May 2024 22:10:47 +0200 Subject: [PATCH 089/215] Change file structure of constraints/types --- .../{types.rs => types/challenge_string.rs} | 16 +------------- src/constraints/types/federation_id.rs | 22 +++++++++++++++++++ src/constraints/types/mod.rs | 10 +++++++++ 3 files changed, 33 insertions(+), 15 deletions(-) rename src/constraints/{types.rs => types/challenge_string.rs} (56%) create mode 100644 src/constraints/types/federation_id.rs create mode 100644 src/constraints/types/mod.rs diff --git a/src/constraints/types.rs b/src/constraints/types/challenge_string.rs similarity index 56% rename from src/constraints/types.rs rename to src/constraints/types/challenge_string.rs index 032b9b7..0f476d9 100644 --- a/src/constraints/types.rs +++ b/src/constraints/types/challenge_string.rs @@ -2,10 +2,8 @@ // 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, ERR_MSG_FEDERATION_ID_REGEX}; +use crate::errors::ERR_MSG_CHALLENGE_STRING_LENGTH; use crate::types::ChallengeString; -use crate::types::{FederationId, REGEX_FEDERATION_ID}; -use regex::Regex; use super::*; @@ -22,15 +20,3 @@ impl Constrained for ChallengeString { Ok(()) } } - -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/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..3741bd2 --- /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/. + +pub mod challenge_string; +pub mod federation_id; + +use crate::certs::Target; +use crate::errors::base::ConstraintError; +use crate::Constrained; From bba616f977230adb26909ca6b680e4c9248fc965 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 21 May 2024 22:13:28 +0200 Subject: [PATCH 090/215] add todo --- src/certs/idcsr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index f2338c3..2ab3b95 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -41,7 +41,7 @@ pub struct IdCsr> { /// [Signature] value for the `inner_csr` pub signature: S, } - +// TODO: Document that we have a SessionId struct that can be used to create valid SessionIds 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]) From a2e275f8d044a9e90cc50d517814eb69f24b3a28 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 23 May 2024 20:12:32 +0200 Subject: [PATCH 091/215] Merge submodules of `error` module --- src/api/mod.rs | 2 +- src/certs/capabilities/basic_constraints.rs | 3 +-- src/certs/capabilities/key_usage.rs | 3 +-- src/certs/capabilities/mod.rs | 7 ++++--- src/certs/idcert.rs | 2 +- src/certs/idcerttbs.rs | 2 +- src/certs/idcsr.rs | 2 +- src/constraints/mod.rs | 2 +- src/constraints/types/mod.rs | 2 +- src/errors/mod.rs | 3 +++ src/key.rs | 2 +- src/types/federation_id.rs | 3 +-- 12 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index a3764e7..93637c2 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,7 +5,7 @@ use serde::Deserialize; use serde_json::from_str; -use crate::errors::composite::RequestError; +use crate::errors::RequestError; #[derive(Debug, Default, Clone)] pub struct HttpClient { diff --git a/src/certs/capabilities/basic_constraints.rs b/src/certs/capabilities/basic_constraints.rs index d627cb5..48c7f42 100644 --- a/src/certs/capabilities/basic_constraints.rs +++ b/src/certs/capabilities/basic_constraints.rs @@ -11,8 +11,7 @@ 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, InvalidInput, ConversionError}; use super::OID_BASIC_CONSTRAINTS; diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 5f8dca1..7ef2508 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::*; diff --git a/src/certs/capabilities/mod.rs b/src/certs/capabilities/mod.rs index 7f35c6e..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"; diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 88f651b..6c13aa5 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -9,7 +9,7 @@ use x509_cert::name::Name; use x509_cert::time::Validity; use x509_cert::Certificate; -use crate::errors::composite::ConversionError; +use crate::errors::ConversionError; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; use crate::Constrained; diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 7d93ad9..9102c74 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -12,7 +12,7 @@ use x509_cert::serial_number::SerialNumber; use x509_cert::time::Validity; use x509_cert::TbsCertificate; -use crate::errors::composite::ConversionError; +use crate::errors::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; use crate::Constrained; diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 2ab3b95..1ef0a34 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -11,7 +11,7 @@ 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; diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs index ec1e6dc..5880d09 100644 --- a/src/constraints/mod.rs +++ b/src/constraints/mod.rs @@ -12,7 +12,7 @@ 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::base::ConstraintError; +use crate::errors::ConstraintError; use crate::key::PublicKey; use crate::signature::Signature; use crate::{ diff --git a/src/constraints/types/mod.rs b/src/constraints/types/mod.rs index 3741bd2..162d757 100644 --- a/src/constraints/types/mod.rs +++ b/src/constraints/types/mod.rs @@ -6,5 +6,5 @@ pub mod challenge_string; pub mod federation_id; use crate::certs::Target; -use crate::errors::base::ConstraintError; +use crate::errors::ConstraintError; use crate::Constrained; diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 3edf7eb..1c87f12 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -24,3 +24,6 @@ pub static ERR_MSG_FEDERATION_ID_REGEX: &str = pub mod base; /// "Composite" error types which consist of one or more "base" error types pub mod composite; + +pub use base::*; +pub use composite::*; diff --git a/src/key.rs b/src/key.rs index f4315fd..1a09fd5 100644 --- a/src/key.rs +++ b/src/key.rs @@ -5,7 +5,7 @@ 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 diff --git a/src/types/federation_id.rs b/src/types/federation_id.rs index bb8cd4f..3014f2e 100644 --- a/src/types/federation_id.rs +++ b/src/types/federation_id.rs @@ -6,8 +6,7 @@ use std::ops::{Deref, DerefMut}; use regex::Regex; -use crate::errors::base::ConstraintError; -use crate::errors::ERR_MSG_FEDERATION_ID_REGEX; +use crate::errors::{ConstraintError, ERR_MSG_FEDERATION_ID_REGEX}; use crate::Constrained; pub static REGEX_FEDERATION_ID: &str = r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)"; From a4f7350d30a4a5735af0b86bce131224539ac99f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 23 May 2024 22:35:53 +0200 Subject: [PATCH 092/215] Add url, http as optional deps --- Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d39517..bb2a413 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ crate-type = ["rlib", "cdylib", "staticlib"] default = ["types"] wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] -types = [] -reqwest = ["dep:reqwest", "types", "serde"] +types = ["dep:http"] +reqwest = ["dep:reqwest", "types", "serde", "dep:url"] serde = ["dep:serde", "dep:serde_json", "dep:ser_der"] [dependencies] @@ -30,6 +30,8 @@ thiserror = "1.0.59" x509-cert = "0.2.5" ser_der = { version = "0.1.0-alpha.1", optional = true, features = ["alloc"] } 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"] } From d6d6b9c81b29eb668b39d6d64127023435a19a7d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 23 May 2024 22:36:43 +0200 Subject: [PATCH 093/215] Start defining some routes --- src/api/core/mod.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/api/core/mod.rs diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs new file mode 100644 index 0000000..e4c78d3 --- /dev/null +++ b/src/api/core/mod.rs @@ -0,0 +1,25 @@ +// 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::certs::idcert::IdCert; +use crate::key::PublicKey; +use crate::signature::Signature; +use crate::types::ChallengeString; + +use super::{HttpClient, HttpResult}; + +// TODO: Use the Routes module to get the correct path for the request + +impl HttpClient { + pub fn get_challenge_string(&self, url: &str) -> HttpResult { + todo!() + } + + pub fn rotate_server_identity_key>( + &self, + url: &str, + ) -> HttpResult> { + todo!() + } +} From d09ade85a7f4f30fc20ba960aaf0c2d73128ece5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 23 May 2024 22:36:56 +0200 Subject: [PATCH 094/215] add core mod --- src/api/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index 93637c2..cb8d7c7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -7,6 +7,8 @@ use serde_json::from_str; use crate::errors::RequestError; +pub mod core; + #[derive(Debug, Default, Clone)] pub struct HttpClient { client: reqwest::Client, @@ -35,6 +37,7 @@ impl HttpClient { url: &str, body: Option, ) -> Result { + // TODO: Parse url using url lib let mut request = self.client.request(method, url); request = request.headers(self.headers.clone()); if let Some(body) = body { From 6072a5341be76aa66ce446524b338c9d237c97d6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 23 May 2024 22:37:18 +0200 Subject: [PATCH 095/215] Define routes module --- src/types/mod.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/types/mod.rs b/src/types/mod.rs index ac86f27..56c768e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,3 +7,26 @@ pub mod federation_id; pub use challenge_string::*; pub use federation_id::*; + +pub mod routes { + pub struct Route { + pub method: http::Method, + pub path: &'static str, + } + + pub mod core { + use 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", + }; + + // TODO: Other routes + } +} From beb82ae953638747e91a5bccbf2bc5c005b5c7be Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 24 May 2024 23:18:31 +0200 Subject: [PATCH 096/215] Add stubs for all (non-mls) routes --- src/api/core/mod.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index e4c78d3..ac1ffe7 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,7 +2,11 @@ // 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 x509_cert::serial_number::SerialNumber; + use crate::certs::idcert::IdCert; +use crate::certs::idcsr::IdCsr; +use crate::certs::{PublicKeyInfo, SessionId}; use crate::key::PublicKey; use crate::signature::Signature; use crate::types::ChallengeString; @@ -10,7 +14,10 @@ use crate::types::ChallengeString; use super::{HttpClient, HttpResult}; // TODO: Use the Routes module to get the correct path for the request +// TODO: Use URL parsing to build the correct URL +// TODO: MLS routes still missing +// Core Routes: No registration needed impl HttpClient { pub fn get_challenge_string(&self, url: &str) -> HttpResult { todo!() @@ -22,4 +29,71 @@ impl HttpClient { ) -> HttpResult> { todo!() } + + pub fn get_server_id_cert>( + &self, + url: &str, + ) -> HttpResult> { + todo!() + } + + pub fn get_server_public_key_info(&self, url: &str) -> HttpResult { + todo!() + } + + pub fn get_actor_id_certs>( + &self, + url: &str, + ) -> HttpResult>> { + todo!() + } + + pub fn update_session_id_cert>( + &self, + url: &str, + new_cert: IdCert, + ) -> HttpResult<()> { + todo!() + } + + pub fn delete_session(&self, url: &str, session_id: &SessionId) -> HttpResult<()> { + todo!() + } +} + +// Core Routes: Registration needed +impl HttpClient { + pub fn rotate_session_id_cert>( + &self, + url: &str, + csr: IdCsr, + ) -> HttpResult<(IdCert, String)> { + todo!() + } + + pub fn upload_encrypted_pkm(&self, url: &str, data: Vec) -> HttpResult<()> { + todo!() + } + + pub fn get_encrypted_pkm( + &self, + url: &str, + serials: Vec, + ) -> HttpResult> { + todo!() + } + + pub fn delete_encrypted_pkm(&self, url: &str, serials: Vec) -> HttpResult<()> { + todo!() + } + + pub fn get_pkm_upload_size_limit(&self, url: &str) -> HttpResult { + todo!() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncryptedPkm { + pub serial: SerialNumber, // TODO[ser_der](bitfl0wer): Impl Serialize, Deserialize for SerialNumber + pub encrypted_pkm: String, } From 61b1b6dc66985c0b2cc2c7c65c882ec6c19c8b52 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 25 May 2024 15:57:11 +0200 Subject: [PATCH 097/215] Add missing route definitions --- src/types/mod.rs | 70 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 56c768e..db1cca8 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,18 +15,68 @@ pub mod routes { } pub mod core { - use super::Route; + pub mod v1 { + use super::super::Route; - pub static GET_CHALLENGE_STRING: Route = Route { - method: http::Method::GET, - path: "/.p2/core/v1/challenge", - }; + 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 ROTATE_SERVER_IDENTITY_KEY: Route = Route { + method: http::Method::PUT, + path: "/.p2/core/v1/key/server", + }; - // TODO: Other routes + 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", + }; + } } } From e9c1f60b5140c567eb50050e4dfdfc955482611b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 14:38:17 +0200 Subject: [PATCH 098/215] Derive Debug and Clone for Route struct --- src/types/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mod.rs b/src/types/mod.rs index db1cca8..95bf7d6 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -9,6 +9,7 @@ pub use challenge_string::*; pub use federation_id::*; pub mod routes { + #[derive(Debug, Clone)] pub struct Route { pub method: http::Method, pub path: &'static str, From 8f5f1068b05b56a8bcd184cc9a6411d73dbd4576 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 14:38:30 +0200 Subject: [PATCH 099/215] Add url::ParseError passthrough --- src/errors/composite.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/errors/composite.rs b/src/errors/composite.rs index 5a41946..bb100f9 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -53,6 +53,8 @@ pub enum RequestError { DeserializationError(#[from] serde_json::Error), #[error("Failed to convert response into expected type")] ConversionError(#[from] ConversionError), + #[error(transparent)] + UrlError(#[from] url::ParseError), } impl From for ConversionError { From fd60f74e2d18ee79da8f359e8b7fff4c190ad14d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 14:38:41 +0200 Subject: [PATCH 100/215] impl get_challenge_string fn --- src/api/core/mod.rs | 56 +++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index ac1ffe7..c2d0be9 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,8 +2,12 @@ // 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::str::FromStr; + +use url::Url; use x509_cert::serial_number::SerialNumber; +use crate::api::core::core::v1::GET_CHALLENGE_STRING; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; use crate::certs::{PublicKeyInfo, SessionId}; @@ -13,42 +17,49 @@ use crate::types::ChallengeString; use super::{HttpClient, HttpResult}; -// TODO: Use the Routes module to get the correct path for the request +use crate::types::routes::*; // TODO: Use URL parsing to build the correct URL // TODO: MLS routes still missing // Core Routes: No registration needed impl HttpClient { - pub fn get_challenge_string(&self, url: &str) -> HttpResult { - todo!() + pub async fn get_challenge_string(&self, url: &str) -> HttpResult { + let parsed_url = Url::from_str(url)?; + let request_url = parsed_url.join(GET_CHALLENGE_STRING.path)?.to_string(); + let request_response = self + .client + .request(GET_CHALLENGE_STRING.method.clone(), request_url) + .send() + .await; + HttpClient::handle_response(request_response).await } - pub fn rotate_server_identity_key>( + pub async fn rotate_server_identity_key>( &self, url: &str, ) -> HttpResult> { todo!() } - pub fn get_server_id_cert>( + pub async fn get_server_id_cert>( &self, url: &str, ) -> HttpResult> { todo!() } - pub fn get_server_public_key_info(&self, url: &str) -> HttpResult { + pub async fn get_server_public_key_info(&self, url: &str) -> HttpResult { todo!() } - pub fn get_actor_id_certs>( + pub async fn get_actor_id_certs>( &self, url: &str, ) -> HttpResult>> { todo!() } - pub fn update_session_id_cert>( + pub async fn update_session_id_cert>( &self, url: &str, new_cert: IdCert, @@ -56,14 +67,14 @@ impl HttpClient { todo!() } - pub fn delete_session(&self, url: &str, session_id: &SessionId) -> HttpResult<()> { + pub async fn delete_session(&self, url: &str, session_id: &SessionId) -> HttpResult<()> { todo!() } } // Core Routes: Registration needed impl HttpClient { - pub fn rotate_session_id_cert>( + pub async fn rotate_session_id_cert>( &self, url: &str, csr: IdCsr, @@ -71,11 +82,11 @@ impl HttpClient { todo!() } - pub fn upload_encrypted_pkm(&self, url: &str, data: Vec) -> HttpResult<()> { + pub async fn upload_encrypted_pkm(&self, url: &str, data: Vec) -> HttpResult<()> { todo!() } - pub fn get_encrypted_pkm( + pub async fn get_encrypted_pkm( &self, url: &str, serials: Vec, @@ -83,11 +94,15 @@ impl HttpClient { todo!() } - pub fn delete_encrypted_pkm(&self, url: &str, serials: Vec) -> HttpResult<()> { + pub async fn delete_encrypted_pkm( + &self, + url: &str, + serials: Vec, + ) -> HttpResult<()> { todo!() } - pub fn get_pkm_upload_size_limit(&self, url: &str) -> HttpResult { + pub async fn get_pkm_upload_size_limit(&self, url: &str) -> HttpResult { todo!() } } @@ -97,3 +112,16 @@ pub struct EncryptedPkm { pub serial: SerialNumber, // TODO[ser_der](bitfl0wer): Impl Serialize, Deserialize for SerialNumber pub encrypted_pkm: String, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_challenge_string() { + let client = HttpClient::new(); + let url = "https://example.com/"; + let result = client.get_challenge_string(url); + assert!(result.is_err()); + } +} From 2d19a48d85815781b89b0cc0711dc22780758127 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 14:54:49 +0200 Subject: [PATCH 101/215] impl pub async fn rotate_server_identity_key --- src/api/core/mod.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index c2d0be9..81dc261 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -7,25 +7,31 @@ use std::str::FromStr; use url::Url; use x509_cert::serial_number::SerialNumber; -use crate::api::core::core::v1::GET_CHALLENGE_STRING; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; use crate::certs::{PublicKeyInfo, SessionId}; use crate::key::PublicKey; use crate::signature::Signature; +use crate::types::routes::core::v1::*; use crate::types::ChallengeString; use super::{HttpClient, HttpResult}; -use crate::types::routes::*; // TODO: Use URL parsing to build the correct URL // TODO: MLS routes still missing +impl HttpClient { + fn normalize_url(url: &str, path: &str) -> Result { + let parsed_url = Url::from_str(url)?; + Ok(parsed_url.join(path)?.to_string()) + } +} + // Core Routes: No registration needed impl HttpClient { + /// Request a [ChallengeString] from the server. pub async fn get_challenge_string(&self, url: &str) -> HttpResult { - let parsed_url = Url::from_str(url)?; - let request_url = parsed_url.join(GET_CHALLENGE_STRING.path)?.to_string(); + let request_url = HttpClient::normalize_url(url, GET_CHALLENGE_STRING.path)?; let request_response = self .client .request(GET_CHALLENGE_STRING.method.clone(), request_url) @@ -34,11 +40,20 @@ impl HttpClient { 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. pub async fn rotate_server_identity_key>( &self, url: &str, ) -> HttpResult> { - todo!() + let request_url = HttpClient::normalize_url(url, 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?; + Ok(IdCert::from_pem(pem.as_str(), None)?) } pub async fn get_server_id_cert>( From 3b7ded2454ea1f778809ae9753d2efa8b7ef26c9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 14:55:03 +0200 Subject: [PATCH 102/215] remove todo --- src/api/core/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 81dc261..03c8ab3 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -17,7 +17,6 @@ use crate::types::ChallengeString; use super::{HttpClient, HttpResult}; -// TODO: Use URL parsing to build the correct URL // TODO: MLS routes still missing impl HttpClient { From 09ed3c255f52e43355dea270abce50bbdd34912c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 15:37:25 +0200 Subject: [PATCH 103/215] Impl {From,To}{Der,Pem} for PublicKeyInfo struct --- src/certs/mod.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 3828bbd..4f5bd5b 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -5,9 +5,12 @@ use std::ops::{Deref, DerefMut}; use der::asn1::{BitString, Ia5String}; +use der::pem::LineEnding; +use der::{Decode, DecodePem, Encode, EncodePem}; use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; use x509_cert::name::Name; +use crate::errors::ConversionError; use crate::{Constrained, ConstraintError, OID_RDN_DOMAIN_COMPONENT}; /// Additional capabilities ([x509_cert::ext::Extensions] or [x509_cert::attr::Attributes], depending @@ -92,6 +95,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 { From 88b66fa20182e37ba0bd561c8c246336eb8d53c7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 15:48:21 +0200 Subject: [PATCH 104/215] impl get_server_id_cert, get_server_public_key_info --- src/api/core/mod.rs | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 03c8ab3..1c8e655 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -4,6 +4,8 @@ use std::str::FromStr; +use http::response; +use serde_json::json; use url::Url; use x509_cert::serial_number::SerialNumber; @@ -52,24 +54,59 @@ impl HttpClient { .send() .await; let pem = HttpClient::handle_response::(request_response).await?; - Ok(IdCert::from_pem(pem.as_str(), None)?) + Ok(IdCert::from_pem( + pem.as_str(), + Some(crate::certs::Target::HomeServer), + )?) } + /// 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. pub async fn get_server_id_cert>( &self, url: &str, + unix_time: Option, ) -> HttpResult> { - todo!() + let request_url = HttpClient::normalize_url(url, 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(format!("{{\"timestamp\": {}}}", time).to_string()); + } + let response = request.send().await; + let pem = HttpClient::handle_response::(response).await?; + Ok(IdCert::from_pem( + pem.as_str(), + Some(crate::certs::Target::HomeServer), + )?) } - pub async fn get_server_public_key_info(&self, url: &str) -> HttpResult { - todo!() + /// Request the server's [PublicKeyInfo]. + pub async fn get_server_public_key_info( + &self, + url: &str, + unix_time: Option, + ) -> HttpResult { + let request_url = HttpClient::normalize_url(url, 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())?) } pub async fn get_actor_id_certs>( &self, url: &str, + fids: &[String], ) -> HttpResult>> { + let request_url = HttpClient::normalize_url(url, GET_ACTOR_IDCERTS.path)?; + todo!() } From e6f277c17bb043241d4c61af1957204b450feb14 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 16:51:07 +0200 Subject: [PATCH 105/215] impl Display for SessionId --- src/certs/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 4f5bd5b..81c05e3 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -49,6 +49,12 @@ 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()`, From 20461b5b339620ef987dc6b8c4847807c2a5b4af Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 16:52:27 +0200 Subject: [PATCH 106/215] impl all "Registration not needed" routes --- src/api/core/mod.rs | 56 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 1c8e655..91c5e74 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,16 +2,17 @@ // 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::os::unix; use std::str::FromStr; -use http::response; +use http::{request, response}; use serde_json::json; use url::Url; use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; -use crate::certs::{PublicKeyInfo, SessionId}; +use crate::certs::{PublicKeyInfo, SessionId, Target}; use crate::key::PublicKey; use crate::signature::Signature; use crate::types::routes::core::v1::*; @@ -72,7 +73,7 @@ impl HttpClient { .client .request(GET_SERVER_PUBLIC_IDCERT.method.clone(), request_url); if let Some(time) = unix_time { - request = request.body(format!("{{\"timestamp\": {}}}", time).to_string()); + request = request.body(json!({ "timestamp": time }).to_string()); } let response = request.send().await; let pem = HttpClient::handle_response::(response).await?; @@ -82,7 +83,9 @@ impl HttpClient { )?) } - /// Request the server's [PublicKeyInfo]. + /// 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, url: &str, @@ -100,26 +103,59 @@ impl HttpClient { 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. pub async fn get_actor_id_certs>( &self, url: &str, - fids: &[String], + fid: &str, + unix_time: Option, ) -> HttpResult>> { - let request_url = HttpClient::normalize_url(url, GET_ACTOR_IDCERTS.path)?; - - todo!() + let request_url = + HttpClient::normalize_url(url, &format!("{}?{}", GET_ACTOR_IDCERTS.path, fid))?; + let mut request = self + .client + .request(GET_ACTOR_IDCERTS.method.clone(), request_url); + if let Some(time) = unix_time { + request = request.body(json!({ "timestamp": time }).to_string()); + } + let response = request.send().await; + let pems = HttpClient::handle_response::>(response).await?; + let mut vec_idcert = Vec::new(); + for pem in pems.into_iter() { + vec_idcert.push(IdCert::::from_pem( + pem.as_str(), + Some(crate::certs::Target::Actor), + )?) + } + Ok(vec_idcert) } + /// Inform a foreign server about a new [IdCert] for a session. pub async fn update_session_id_cert>( &self, url: &str, new_cert: IdCert, ) -> HttpResult<()> { - todo!() + let request_url = HttpClient::normalize_url(url, 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, url: &str, session_id: &SessionId) -> HttpResult<()> { - todo!() + let request_url = + HttpClient::normalize_url(url, &format!("{}{}", DELETE_SESSION.path, session_id))?; + self.client + .request(DELETE_SESSION.method.clone(), request_url) + .send() + .await?; + Ok(()) } } From 9953c270e8233cb6ee350a67c715aed1f8999696 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 16:53:18 +0200 Subject: [PATCH 107/215] Remove imports, fix "test" --- src/api/core/mod.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 91c5e74..86c4f0f 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,17 +2,15 @@ // 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::os::unix; use std::str::FromStr; -use http::{request, response}; use serde_json::json; use url::Url; use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; -use crate::certs::{PublicKeyInfo, SessionId, Target}; +use crate::certs::{PublicKeyInfo, SessionId}; use crate::key::PublicKey; use crate::signature::Signature; use crate::types::routes::core::v1::*; @@ -208,7 +206,6 @@ mod test { fn test_get_challenge_string() { let client = HttpClient::new(); let url = "https://example.com/"; - let result = client.get_challenge_string(url); - assert!(result.is_err()); + let _result = client.get_challenge_string(url); } } From e0dcbe988b090a03a154f410e197beedf8dddce3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 21:16:16 +0200 Subject: [PATCH 108/215] Change API of HttpClient --- src/api/core/mod.rs | 53 +++++++++++++++------------------------------ src/api/mod.rs | 52 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 86c4f0f..c9e287e 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,10 +2,7 @@ // 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::str::FromStr; - use serde_json::json; -use url::Url; use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; @@ -20,18 +17,11 @@ use super::{HttpClient, HttpResult}; // TODO: MLS routes still missing -impl HttpClient { - fn normalize_url(url: &str, path: &str) -> Result { - let parsed_url = Url::from_str(url)?; - Ok(parsed_url.join(path)?.to_string()) - } -} - // Core Routes: No registration needed impl HttpClient { /// Request a [ChallengeString] from the server. - pub async fn get_challenge_string(&self, url: &str) -> HttpResult { - let request_url = HttpClient::normalize_url(url, GET_CHALLENGE_STRING.path)?; + 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) @@ -44,9 +34,8 @@ impl HttpClient { /// only available to server administrators. pub async fn rotate_server_identity_key>( &self, - url: &str, ) -> HttpResult> { - let request_url = HttpClient::normalize_url(url, ROTATE_SERVER_IDENTITY_KEY.path)?; + 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) @@ -63,10 +52,9 @@ impl HttpClient { /// valid at that time. If no timestamp is provided, the current IdCert is returned. pub async fn get_server_id_cert>( &self, - url: &str, unix_time: Option, ) -> HttpResult> { - let request_url = HttpClient::normalize_url(url, GET_SERVER_PUBLIC_IDCERT.path)?; + 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); @@ -86,10 +74,9 @@ impl HttpClient { /// returned. pub async fn get_server_public_key_info( &self, - url: &str, unix_time: Option, ) -> HttpResult { - let request_url = HttpClient::normalize_url(url, GET_SERVER_PUBLIC_KEY.path)?; + 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); @@ -106,12 +93,12 @@ impl HttpClient { /// time. If no timestamp is provided, the current IdCerts are returned. pub async fn get_actor_id_certs>( &self, - url: &str, fid: &str, unix_time: Option, ) -> HttpResult>> { - let request_url = - HttpClient::normalize_url(url, &format!("{}?{}", GET_ACTOR_IDCERTS.path, fid))?; + 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); @@ -133,10 +120,9 @@ impl HttpClient { /// Inform a foreign server about a new [IdCert] for a session. pub async fn update_session_id_cert>( &self, - url: &str, new_cert: IdCert, ) -> HttpResult<()> { - let request_url = HttpClient::normalize_url(url, UPDATE_SESSION_IDCERT.path)?; + 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)?) @@ -146,9 +132,10 @@ impl HttpClient { } /// Tell a server to delete a session, revoking the session token. - pub async fn delete_session(&self, url: &str, session_id: &SessionId) -> HttpResult<()> { - let request_url = - HttpClient::normalize_url(url, &format!("{}{}", DELETE_SESSION.path, session_id))?; + pub async fn delete_session(&self, session_id: &SessionId) -> HttpResult<()> { + let request_url = self + .url + .join(&format!("{}{}", DELETE_SESSION.path, session_id))?; self.client .request(DELETE_SESSION.method.clone(), request_url) .send() @@ -161,29 +148,23 @@ impl HttpClient { impl HttpClient { pub async fn rotate_session_id_cert>( &self, - url: &str, csr: IdCsr, ) -> HttpResult<(IdCert, String)> { todo!() } - pub async fn upload_encrypted_pkm(&self, url: &str, data: Vec) -> HttpResult<()> { + pub async fn upload_encrypted_pkm(&self, data: Vec) -> HttpResult<()> { todo!() } pub async fn get_encrypted_pkm( &self, - url: &str, serials: Vec, ) -> HttpResult> { todo!() } - pub async fn delete_encrypted_pkm( - &self, - url: &str, - serials: Vec, - ) -> HttpResult<()> { + pub async fn delete_encrypted_pkm(&self, serials: Vec) -> HttpResult<()> { todo!() } @@ -204,8 +185,8 @@ mod test { #[test] fn test_get_challenge_string() { - let client = HttpClient::new(); let url = "https://example.com/"; - let _result = client.get_challenge_string(url); + let client = HttpClient::new(url).unwrap(); + let _result = client.get_challenge_string(); } } diff --git a/src/api/mod.rs b/src/api/mod.rs index cb8d7c7..1adaba7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,38 +4,74 @@ use serde::Deserialize; use serde_json::from_str; +use url::Url; use crate::errors::RequestError; pub mod core; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] +/// A client for making HTTP requests to a polyproto home server. +/// +/// # 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 { client: reqwest::Client, headers: reqwest::header::HeaderMap, + pub(crate) url: Url, } pub type HttpResult = Result; impl HttpClient { - pub fn new() -> Self { + /// Creates a new instance of the client with no further configuration. A client initialized + /// with this method can not be used for any requests that require authentication. + /// + /// # 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(); - Self { client, headers } + let url = Url::parse(url)?; + + Ok(Self { + client, + headers, + url, + }) } - /// Creates a new instance of the client with the provided headers. - pub fn with_headers(mut self, headers: reqwest::header::HeaderMap) -> Self { + /// Sets the headers for the client. + pub fn headers(&mut self, headers: reqwest::header::HeaderMap) { self.headers = headers; - self + } + + /// 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( + pub async fn request>( &self, method: reqwest::Method, url: &str, - body: Option, + body: Option, ) -> Result { // TODO: Parse url using url lib let mut request = self.client.request(method, url); From da77b0169ebaa7ea8873ea3f48da5acc935efcc7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 21:34:28 +0200 Subject: [PATCH 109/215] Start implementing Core Routes: Registration needed --- src/api/core/mod.rs | 103 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index c9e287e..58943c5 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,12 +2,14 @@ // 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, Serialize}; use serde_json::json; use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; use crate::certs::{PublicKeyInfo, SessionId}; +use crate::errors::ConversionError; use crate::key::PublicKey; use crate::signature::Signature; use crate::types::routes::core::v1::*; @@ -146,22 +148,76 @@ impl HttpClient { // 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. pub async fn rotate_session_id_cert>( &self, csr: IdCsr, ) -> HttpResult<(IdCert, String)> { - todo!() + 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 (pem, token) = + HttpClient::handle_response::<(String, String)>(request_response).await?; + Ok(( + IdCert::from_pem(pem.as_str(), Some(crate::certs::Target::Actor))?, + 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. pub async fn upload_encrypted_pkm(&self, data: Vec) -> HttpResult<()> { - todo!() + let mut body = Vec::new(); + for pkm in data.iter() { + body.push(json!({ + "serial_number": pkm.serial_number.to_string(), + "encrypted_pkm": pkm.key_data + })); + } + 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> { - todo!() + let request_url = self.url.join(GET_ENCRYPTED_PKM.path)?; + let mut body = Vec::new(); + for serial in serials.iter() { + body.push(json!(serial.to_string())); + } + let request = self + .client + .request(GET_ENCRYPTED_PKM.method.clone(), request_url) + .body( + json!({ + "serial_numbers": 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(EncryptedPkm::try_from(pkm)?); + } + Ok(vec_pkm) } pub async fn delete_encrypted_pkm(&self, serials: Vec) -> HttpResult<()> { @@ -174,9 +230,46 @@ impl HttpClient { } #[derive(Debug, Clone, PartialEq, Eq)] +/// Represents encrypted private key material. The `serial` is used to identify the key +/// material. The `encrypted_pkm` is the actual encrypted private key material. pub struct EncryptedPkm { - pub serial: SerialNumber, // TODO[ser_der](bitfl0wer): Impl Serialize, Deserialize for SerialNumber - pub encrypted_pkm: String, + pub serial_number: SerialNumber, + pub key_data: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// Stringly typed version of [EncryptedPkm], used for serialization and deserialization. +/// This is necessary because [SerialNumber] from the `x509_cert` crate does not implement +/// `Serialize` and `Deserialize`. +/// +/// Implements `From` and `TryInto` for conversion between the two +/// types. +/// +/// (actually, it does not implement `TryInto`. However, [EncryptedPkm] implements +/// `TryFrom`, but you get the idea.) +pub struct EncryptedPkmJson { + pub serial_number: String, + pub key_data: String, +} + +impl From for EncryptedPkmJson { + fn from(pkm: EncryptedPkm) -> Self { + Self { + serial_number: pkm.serial_number.to_string(), + key_data: pkm.key_data, + } + } +} + +impl TryFrom for EncryptedPkm { + type Error = ConversionError; + + fn try_from(pkm: EncryptedPkmJson) -> Result { + Ok(Self { + serial_number: SerialNumber::new(pkm.serial_number.as_bytes())?, + key_data: pkm.key_data, + }) + } } #[cfg(test)] From caca86690bf536ceb53d5cd34361199fdaaea69c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 27 May 2024 22:20:56 +0200 Subject: [PATCH 110/215] impl rest of routes --- src/api/core/mod.rs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 58943c5..4c608c8 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -205,12 +205,7 @@ impl HttpClient { let request = self .client .request(GET_ENCRYPTED_PKM.method.clone(), request_url) - .body( - json!({ - "serial_numbers": body - }) - .to_string(), - ); + .body(json!(body).to_string()); let response = HttpClient::handle_response::>(request.send().await).await?; let mut vec_pkm = Vec::new(); @@ -220,12 +215,30 @@ impl HttpClient { 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<()> { - todo!() + let request_url = self.url.join(DELETE_ENCRYPTED_PKM.path)?; + let mut body = Vec::new(); + for serial in serials.iter() { + body.push(json!(serial.to_string())); + } + self.client + .request(DELETE_ENCRYPTED_PKM.method.clone(), request_url) + .body(json!(body).to_string()) + .send() + .await?; + Ok(()) } - pub async fn get_pkm_upload_size_limit(&self, url: &str) -> HttpResult { - todo!() + /// 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 } } From 9a7ae88b20bfaf3206ec72298064cfcb316c5cf8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 11:22:58 +0200 Subject: [PATCH 111/215] Write better documentation, make client in HttpClient pub --- src/api/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 1adaba7..b2fa97f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -11,7 +11,10 @@ use crate::errors::RequestError; pub mod core; #[derive(Debug, Clone)] -/// A client for making HTTP requests to a polyproto home server. +/// 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 /// @@ -24,7 +27,7 @@ pub mod core; /// let challenge: ChallengeString = client.get_challenge_string().await.unwrap(); /// ``` pub struct HttpClient { - client: reqwest::Client, + pub client: reqwest::Client, headers: reqwest::header::HeaderMap, pub(crate) url: Url, } @@ -32,8 +35,8 @@ pub struct HttpClient { pub type HttpResult = Result; impl HttpClient { - /// Creates a new instance of the client with no further configuration. A client initialized - /// with this method can not be used for any requests that require authentication. + /// 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 /// From c9fd972383c2c90912532e48e01c4032c447a8e9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 15:16:05 +0200 Subject: [PATCH 112/215] improve api design of get_actor_id_certs route --- src/api/core/mod.rs | 46 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 4c608c8..a6e5e72 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -97,7 +97,7 @@ impl HttpClient { &self, fid: &str, unix_time: Option, - ) -> HttpResult>> { + ) -> HttpResult>> { let request_url = self .url .join(&format!("{}?{}", GET_ACTOR_IDCERTS.path, fid))?; @@ -108,13 +108,10 @@ impl HttpClient { request = request.body(json!({ "timestamp": time }).to_string()); } let response = request.send().await; - let pems = HttpClient::handle_response::>(response).await?; + let pems = HttpClient::handle_response::>(response).await?; let mut vec_idcert = Vec::new(); for pem in pems.into_iter() { - vec_idcert.push(IdCert::::from_pem( - pem.as_str(), - Some(crate::certs::Target::Actor), - )?) + vec_idcert.push(IdCertExt::try_from(pem)?); } Ok(vec_idcert) } @@ -285,6 +282,43 @@ impl TryFrom for EncryptedPkm { } } +#[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> { + pub id_cert: IdCert, + pub invalidated: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +/// Stringly typed version of [IdCertExt], used for serialization and deserialization. +pub struct IdCertExtJson { + pub id_cert: String, + 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(id_cert.id_cert.as_str(), Some(crate::certs::Target::Actor))?, + invalidated: id_cert.invalidated, + }) + } +} + #[cfg(test)] mod test { use super::*; From 3c13f76629d9db67efc49b890e7884dda43d7add Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 17:53:30 +0200 Subject: [PATCH 113/215] more clear variable naming --- src/api/core/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index a6e5e72..3155b59 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -110,8 +110,8 @@ impl HttpClient { let response = request.send().await; let pems = HttpClient::handle_response::>(response).await?; let mut vec_idcert = Vec::new(); - for pem in pems.into_iter() { - vec_idcert.push(IdCertExt::try_from(pem)?); + for json in pems.into_iter() { + vec_idcert.push(IdCertExt::try_from(json)?); } Ok(vec_idcert) } From 9603d7c871657ebce514da11e50479f9123e6674 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 19:42:00 +0200 Subject: [PATCH 114/215] Publish alpha 8 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bb2a413..8f3b07e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.7" +version = "0.9.0-alpha.8" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From 44fae93a713a6e99a286ce81cb6a60ea078c2673 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 21:25:46 +0200 Subject: [PATCH 115/215] Include httptest for HTTP server mocking in tests --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 8f3b07e..8474d3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ 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" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] From 58395b6b4be3e282e5498111b2cdee57f5fbc3d5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 21:26:55 +0200 Subject: [PATCH 116/215] Create tests/api/core --- tests/api/core/mod.rs | 3 +++ tests/api/mod.rs | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 tests/api/core/mod.rs diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs new file mode 100644 index 0000000..7be716e --- /dev/null +++ b/tests/api/core/mod.rs @@ -0,0 +1,3 @@ +// 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/. diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 3f8df27..7482830 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -2,6 +2,8 @@ // 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; From 4e41447fa63f99d2b1d093de47f904aa5ddbd7e3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 21:29:59 +0200 Subject: [PATCH 117/215] fix todo --- src/api/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index b2fa97f..6483d62 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -75,14 +75,14 @@ impl HttpClient { method: reqwest::Method, url: &str, body: Option, - ) -> Result { - // TODO: Parse url using url lib + ) -> 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); } - request.send().await + Ok(request.send().await?) } /// Sends a request, handles the response, and returns the deserialized object. From 766b59dc1298363f754987d6b723f4f43bf5df22 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 22:13:01 +0200 Subject: [PATCH 118/215] exclude static defs from code cov --- src/types/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mod.rs b/src/types/mod.rs index 95bf7d6..1a9c813 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,6 +15,7 @@ pub mod routes { pub path: &'static str, } + #[cfg(not(tarpaulin_include))] pub mod core { pub mod v1 { use super::super::Route; From 0b94d2f0d49aea15d1818352ad0b108ebf6c91f1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 22:44:14 +0200 Subject: [PATCH 119/215] Add tokio for trsting async fns --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 8474d3d..edfe904 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } env_logger = "0.11.3" httptest = "0.16.1" rand = "0.8.5" +tokio = { version = "1.37.0", features = ["full"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.42" From 8e465acb23e09028ec3c6d66893cf6fb4e8add5a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 28 May 2024 22:45:10 +0200 Subject: [PATCH 120/215] Add first test for HTTP APIs --- tests/api/core/mod.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 7be716e..9bd56a0 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -1,3 +1,35 @@ // 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 httptest::matchers::request; +use httptest::responders::json_encoded; +use httptest::*; +use serde_json::json; + +use crate::common::init_logger; + +/// 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", "/.p2/core/v1/challenge")).respond_with( + json_encoded(json!({ + "challenge": "a".repeat(32), + "expires": 1 + })), + ), + ); + let url = server_url(&server); + dbg!(url.clone()); + 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); +} From aa262cd23b804e1af58320f0b9bb62c9ad5be08f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 10:39:22 +0200 Subject: [PATCH 121/215] update documentation --- src/certs/idcsr.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 1ef0a34..972e2c8 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -57,8 +57,9 @@ impl> IdCsr { /// - Organizational Unit: Optional. May be repeated. /// - **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. @@ -171,6 +172,9 @@ impl> IdCsrInner { /// /// 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, From 9dd4ed996a54d797648bc17f8d30342baef70cdd Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 10:39:29 +0200 Subject: [PATCH 122/215] update dev-deps --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index edfe904..d66add4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ env_logger = "0.11.3" httptest = "0.16.1" rand = "0.8.5" tokio = { version = "1.37.0", features = ["full"] } +serde = { version = "1.0.199", features = ["derive"] } +serde_json = { version = "1.0.116" } +polyproto = { path = "./", features = ["types", "reqwest", "serde"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.42" From 2977a44000c333e6f03a19ca3928fe3138fc9392 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 10:39:34 +0200 Subject: [PATCH 123/215] add debug log --- src/api/core/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 3155b59..1b2b1f6 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -44,6 +44,7 @@ impl HttpClient { .send() .await; let pem = HttpClient::handle_response::(request_response).await?; + log::debug!("Received IdCert: \n{}", pem); Ok(IdCert::from_pem( pem.as_str(), Some(crate::certs::Target::HomeServer), From 2086fbe80f5fbc0a025907a5446eb44e64eda01a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 10:39:46 +0200 Subject: [PATCH 124/215] start impl rotate_server_identity_key --- tests/api/core/mod.rs | 79 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 9bd56a0..c2ef318 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -2,12 +2,23 @@ // 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 httptest::matchers::request; -use httptest::responders::json_encoded; +use std::str::FromStr; +use std::time::Duration; + +use der::asn1::{Uint, UtcTime}; +use httptest::matchers::request::method_path; +use httptest::matchers::{eq, json_decoded, request}; +use httptest::responders::{json_encoded, status_code}; use httptest::*; +use polyproto::certs::capabilities::Capabilities; +use polyproto::certs::idcert::IdCert; +use polyproto::certs::idcsr::IdCsr; +use polyproto::types::routes::core::v1::{GET_CHALLENGE_STRING, ROTATE_SERVER_IDENTITY_KEY}; +use polyproto::Name; use serde_json::json; +use x509_cert::time::{Time, Validity}; -use crate::common::init_logger; +use crate::common::{init_logger, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature}; /// Correctly format the server URL for the test. fn server_url(server: &Server) -> String { @@ -19,17 +30,65 @@ async fn get_challenge_string() { init_logger(); let server = Server::run(); server.expect( - Expectation::matching(request::method_path("GET", "/.p2/core/v1/challenge")).respond_with( - json_encoded(json!({ - "challenge": "a".repeat(32), - "expires": 1 - })), - ), + 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); - dbg!(url.clone()); 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 mut csprng = rand::rngs::OsRng; + let subject = Name::from_str("CN=root,DC=polyphony,DC=chat").unwrap(); + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + let id_csr = IdCsr::::new( + &subject, + &priv_key, + &Capabilities::default_home_server(), + Some(polyproto::certs::Target::HomeServer), + ) + .unwrap(); + let id_cert = IdCert::from_ca_csr( + id_csr, + &priv_key, + Uint::new(&[8]).unwrap(), + subject, + 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_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); +} From 172fc4013da5ea9a1f391c41ab6afc52731dc27b Mon Sep 17 00:00:00 2001 From: Flori Weber Date: Wed, 29 May 2024 14:26:57 +0200 Subject: [PATCH 125/215] Test converting home server cert from PEM --- tests/certificates/idcert.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/certificates/idcert.rs b/tests/certificates/idcert.rs index 5538c18..9e870f0 100644 --- a/tests/certificates/idcert.rs +++ b/tests/certificates/idcert.rs @@ -191,6 +191,33 @@ fn cert_from_pem() { ) .unwrap(); let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_der = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); - assert_eq!(cert_from_der, cert) + let cert_from_pem = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); + 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, + &Capabilities::default_home_server(), + Some(Target::HomeServer), + ) + .unwrap(); + 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 data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let cert_from_pem = + IdCert::from_pem(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); + assert_eq!(cert_from_pem, cert); } From b4fad48a4588b21e39d607afde9314b4787c4d46 Mon Sep 17 00:00:00 2001 From: Flori Weber Date: Wed, 29 May 2024 14:32:08 +0200 Subject: [PATCH 126/215] Merge branch 'v0.9' of https://github.com/polyphony-chat/polyproto into v0.9 --- tests/certificates/idcert.rs | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/tests/certificates/idcert.rs b/tests/certificates/idcert.rs index 9e870f0..5538c18 100644 --- a/tests/certificates/idcert.rs +++ b/tests/certificates/idcert.rs @@ -191,33 +191,6 @@ fn cert_from_pem() { ) .unwrap(); let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_pem = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); - 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, - &Capabilities::default_home_server(), - Some(Target::HomeServer), - ) - .unwrap(); - 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 data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_pem = - IdCert::from_pem(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); - assert_eq!(cert_from_pem, cert); + let cert_from_der = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); + assert_eq!(cert_from_der, cert) } From 4740dce65cbf97d6468f1c22ab77b9ce490fcbd3 Mon Sep 17 00:00:00 2001 From: Flori Weber Date: Wed, 29 May 2024 14:55:56 +0200 Subject: [PATCH 127/215] bitstring experiments --- tests/certificates/idcert.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/certificates/idcert.rs b/tests/certificates/idcert.rs index 5538c18..8930b6a 100644 --- a/tests/certificates/idcert.rs +++ b/tests/certificates/idcert.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use std::time::Duration; use der::asn1::{BitString, Ia5String, Uint, UtcTime}; -use der::Encode; +use der::{Decode, Encode}; use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; use polyproto::certs::capabilities::{self, Capabilities}; use polyproto::certs::idcert::IdCert; @@ -194,3 +194,23 @@ fn cert_from_pem() { let cert_from_der = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); assert_eq!(cert_from_der, cert) } + +#[test] +fn test_bitstings() { + init_logger(); + let data = 255u8.to_be_bytes(); + let bitstring = BitString::new(0, data).unwrap(); + log::debug!("Bitstring: {:#?}", bitstring); + let der = bitstring.to_der().unwrap(); + let any = der::Any::from_der(&der).unwrap(); + log::debug!("Any: {:#?}", any); + log::debug!("Bitstring: {:#?}", BitString::from_der(&der).unwrap()); + let bitstring_from_any_value = BitString::from_bytes(any.value()).unwrap(); // Doesn't work! + log::debug!( + "Bitstring from Any value(): {:#?}", + bitstring_from_any_value + ); + //assert_eq!(bitstring, bitstring_from_any_value); + log::debug!("raw der bitstring {:?}", bitstring.to_der().unwrap()); + log::debug!("raw bitstring {:?}", bitstring.raw_bytes()); // Raw bytes is [255], don't use this +} From 15c219764d31dc62558c3ad6ccc4b9985b5b8ffe Mon Sep 17 00:00:00 2001 From: Flori Weber Date: Wed, 29 May 2024 14:55:57 +0200 Subject: [PATCH 128/215] experiments with bitstrings --- tests/certificates/idcert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/certificates/idcert.rs b/tests/certificates/idcert.rs index 8930b6a..af12d2e 100644 --- a/tests/certificates/idcert.rs +++ b/tests/certificates/idcert.rs @@ -210,7 +210,7 @@ fn test_bitstings() { "Bitstring from Any value(): {:#?}", bitstring_from_any_value ); - //assert_eq!(bitstring, bitstring_from_any_value); + //assert_eq!(bitstring, bitstring_from_any_value); // NOT THE SAME! log::debug!("raw der bitstring {:?}", bitstring.to_der().unwrap()); log::debug!("raw bitstring {:?}", bitstring.raw_bytes()); // Raw bytes is [255], don't use this } From 4b20f7a6b5a3a3c8639dfa44a8c25fdf438b086b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 19:09:43 +0200 Subject: [PATCH 129/215] Fix incorrect bitstring conversions --- src/certs/capabilities/key_usage.rs | 4 ++-- tests/api/core/mod.rs | 4 ++-- tests/certificates/idcert.rs | 3 +++ tests/common/mod.rs | 10 ++++------ 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 7ef2508..6d37533 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -253,7 +253,7 @@ 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())?) + KeyUsages::from_bitstring(BitString::from_der(&inner_value.to_der()?)?) } } @@ -270,7 +270,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()?)?) } } diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index c2ef318..753363b 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -6,9 +6,9 @@ use std::str::FromStr; use std::time::Duration; use der::asn1::{Uint, UtcTime}; +use httptest::matchers::request; use httptest::matchers::request::method_path; -use httptest::matchers::{eq, json_decoded, request}; -use httptest::responders::{json_encoded, status_code}; +use httptest::responders::json_encoded; use httptest::*; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; diff --git a/tests/certificates/idcert.rs b/tests/certificates/idcert.rs index af12d2e..e78f581 100644 --- a/tests/certificates/idcert.rs +++ b/tests/certificates/idcert.rs @@ -204,6 +204,7 @@ fn test_bitstings() { let der = bitstring.to_der().unwrap(); let any = der::Any::from_der(&der).unwrap(); log::debug!("Any: {:#?}", any); + log::debug!("Any as bytes: {:#?}", any.to_der().unwrap()); log::debug!("Bitstring: {:#?}", BitString::from_der(&der).unwrap()); let bitstring_from_any_value = BitString::from_bytes(any.value()).unwrap(); // Doesn't work! log::debug!( @@ -211,6 +212,8 @@ fn test_bitstings() { bitstring_from_any_value ); //assert_eq!(bitstring, bitstring_from_any_value); // NOT THE SAME! + let bitstring_from_any_der = BitString::from_der(&any.to_der().unwrap()).unwrap(); + log::debug!("Bitstring from Any to_der(): {:#?}", bitstring_from_any_der); log::debug!("raw der bitstring {:?}", bitstring.to_der().unwrap()); log::debug!("raw bitstring {:?}", bitstring.raw_bytes()); // Raw bytes is [255], don't use this } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c1f33a4..8874730 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -15,12 +15,10 @@ use rand::rngs::OsRng; use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; pub fn init_logger() { - std::env::set_var("RUST_LOG", "trace"); - env_logger::builder() - .filter_module("crate", log::LevelFilter::Trace) - .is_test(true) - .try_init() - .unwrap_or(()); + 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(()); } #[derive(Debug, PartialEq, Eq, Clone)] From 03fa7b3b22b68fb45148d36b47ab392041cc812a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:18:20 +0200 Subject: [PATCH 130/215] rename certificates module to certs --- tests/{certificates => certs}/idcert.rs | 49 ++++++++++++++++++++++--- tests/{certificates => certs}/idcsr.rs | 0 tests/{certificates => certs}/mod.rs | 1 + tests/mod.rs | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) rename tests/{certificates => certs}/idcert.rs (83%) rename tests/{certificates => certs}/idcsr.rs (100%) rename tests/{certificates => certs}/mod.rs (92%) diff --git a/tests/certificates/idcert.rs b/tests/certs/idcert.rs similarity index 83% rename from tests/certificates/idcert.rs rename to tests/certs/idcert.rs index e78f581..8633ca2 100644 --- a/tests/certificates/idcert.rs +++ b/tests/certs/idcert.rs @@ -172,7 +172,7 @@ fn cert_from_pem() { ) .unwrap(); - let mut cert = IdCert::from_actor_csr( + let cert = IdCert::from_actor_csr( csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), @@ -191,12 +191,47 @@ fn cert_from_pem() { ) .unwrap(); let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_der = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); - assert_eq!(cert_from_der, cert) + let cert_from_pem = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).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, + &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 data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); + let cert_from_pem = + IdCert::from_pem(&data, Some(polyproto::certs::Target::HomeServer)).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); } #[test] -fn test_bitstings() { +fn test_bitstrings() { init_logger(); let data = 255u8.to_be_bytes(); let bitstring = BitString::new(0, data).unwrap(); @@ -213,7 +248,11 @@ fn test_bitstings() { ); //assert_eq!(bitstring, bitstring_from_any_value); // NOT THE SAME! let bitstring_from_any_der = BitString::from_der(&any.to_der().unwrap()).unwrap(); - log::debug!("Bitstring from Any to_der(): {:#?}", bitstring_from_any_der); + log::debug!("Bitstring from Any to_der(): {:#?}", bitstring_from_any_der); // ok + log::debug!( + "Raw bitstring from any to der: {:?}", + bitstring_from_any_der.raw_bytes() + ); log::debug!("raw der bitstring {:?}", bitstring.to_der().unwrap()); log::debug!("raw bitstring {:?}", bitstring.raw_bytes()); // Raw bytes is [255], don't use this } diff --git a/tests/certificates/idcsr.rs b/tests/certs/idcsr.rs similarity index 100% rename from tests/certificates/idcsr.rs rename to tests/certs/idcsr.rs diff --git a/tests/certificates/mod.rs b/tests/certs/mod.rs similarity index 92% rename from tests/certificates/mod.rs rename to tests/certs/mod.rs index cc6fc54..c3052d7 100644 --- a/tests/certificates/mod.rs +++ b/tests/certs/mod.rs @@ -2,5 +2,6 @@ // 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/mod.rs b/tests/mod.rs index fba19ce..3a16bbd 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. pub(crate) mod api; -pub(crate) mod certificates; +pub(crate) mod certs; pub(crate) mod common; use polyproto::Constrained; From fa975d794a5af0886295938265a89c5a59f4263b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:18:47 +0200 Subject: [PATCH 131/215] define tests for to_bitstring and from_bitstring for KeyUsages --- tests/certs/capabilities/key_usage.rs | 84 +++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/certs/capabilities/key_usage.rs diff --git a/tests/certs/capabilities/key_usage.rs b/tests/certs/capabilities/key_usage.rs new file mode 100644 index 0000000..c488edb --- /dev/null +++ b/tests/certs/capabilities/key_usage.rs @@ -0,0 +1,84 @@ +// 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 key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); + assert_eq!( + key_usages.key_usages, + vec![ + KeyUsage::DigitalSignature, + KeyUsage::ContentCommitment, + KeyUsage::KeyEncipherment, + KeyUsage::DataEncipherment, + KeyUsage::KeyAgreement, + KeyUsage::KeyCertSign, + KeyUsage::CrlSign, + KeyUsage::EncipherOnly, + KeyUsage::DecipherOnly + ] + ); +} From 311ac4c43dd81cc55c81bb2112bafea0c095eb70 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:18:54 +0200 Subject: [PATCH 132/215] trace logs --- src/constraints/name.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constraints/name.rs b/src/constraints/name.rs index 8f49ddd..554247c 100644 --- a/src/constraints/name.rs +++ b/src/constraints/name.rs @@ -145,7 +145,7 @@ fn validate_dc_matches_dc_in_uid( 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() { - debug!("Checking if component \"{}\"...", component); + trace!("Checking if component \"{}\"...", component); let equivalent_dc = match vec_dc.get(index as usize) { Some(dc) => dc, None => { @@ -155,7 +155,7 @@ fn validate_dc_matches_dc_in_uid( } }; let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); - debug!( + trace!( "...is equal to component \"{}\"...", equivalent_dc.to_string() ); @@ -176,7 +176,7 @@ fn validate_dc_matches_dc_in_uid( Ok(()) } -use log::debug; +use log::trace; use x509_cert::attr::AttributeTypeAndValue; fn validate_rdn_uid(item: &AttributeTypeAndValue) -> Result<(), ConstraintError> { From 96a0bc314e71d9df8da9fddf1d688d7043688958 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:19:00 +0200 Subject: [PATCH 133/215] add mod_key usage --- tests/certs/capabilities/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/certs/capabilities/mod.rs 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; From 2238db9768b7407321f368285c5ef1aa80b5930a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:51:43 +0200 Subject: [PATCH 134/215] More from_bitstring testcases --- tests/certs/capabilities/key_usage.rs | 60 ++++++++++++++++++++------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/tests/certs/capabilities/key_usage.rs b/tests/certs/capabilities/key_usage.rs index c488edb..0f0e301 100644 --- a/tests/certs/capabilities/key_usage.rs +++ b/tests/certs/capabilities/key_usage.rs @@ -66,19 +66,49 @@ fn to_bitstring() { #[test] fn from_bitstring() { let bitstring = BitString::new(7, [128, 255]).unwrap(); - let key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); - assert_eq!( - key_usages.key_usages, - vec![ - KeyUsage::DigitalSignature, - KeyUsage::ContentCommitment, - KeyUsage::KeyEncipherment, - KeyUsage::DataEncipherment, - KeyUsage::KeyAgreement, - KeyUsage::KeyCertSign, - KeyUsage::CrlSign, - KeyUsage::EncipherOnly, - KeyUsage::DecipherOnly - ] - ); + 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); } From dd7ec8712fe653dc88f9c7ede1a20dbfd53236ea Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:52:09 +0200 Subject: [PATCH 135/215] Fix bitstring encoded and decoding, for the third(?) time --- src/certs/capabilities/key_usage.rs | 47 ++++++----------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 6d37533..846cd13 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -90,6 +90,7 @@ impl KeyUsages { /// ``` pub fn from_bitstring(bitstring: BitString) -> Result { let mut byte_array = bitstring.raw_bytes().to_vec(); + 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 @@ -125,17 +126,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 }) } @@ -189,6 +191,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") } @@ -204,38 +208,6 @@ impl TryFrom for KeyUsages { type Error = ConversionError; fn try_from(value: Attribute) -> Result { - // The issue seems to be that the BitString is invalid. - /* - Good BitString: - Any { - tag: Tag(0x03: BIT STRING), - value: BytesOwned { - length: Length( - 4, - ), - inner: [ - 3, - 2, - 0, - 255, - ], - }, - } - - Bad BitString: - Any { - tag: Tag(0x03: BIT STRING), - value: BytesOwned { - length: Length( - 2, - ), - inner: [ - 0, <- Missing Tag "3", Missing Length "2" - 128, - ], - }, - } - */ if value.tag() != Tag::Sequence { return Err(ConversionError::InvalidInput(InvalidInput::Malformed( format!("Expected Sequence, found {}", value.tag(),), @@ -253,6 +225,7 @@ 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"); + log::debug!("Inner value: {:?}", inner_value); KeyUsages::from_bitstring(BitString::from_der(&inner_value.to_der()?)?) } } @@ -280,7 +253,7 @@ impl TryFrom for Attribute { fn try_from(value: KeyUsages) -> Result { let mut sov = SetOfVec::new(); let bitstring = value.to_bitstring(); - let any = Any::new(Tag::BitString, bitstring.to_der()?)?; + let any = Any::from_der(&bitstring.to_der()?)?; sov.insert(any)?; Ok(Attribute { oid: ObjectIdentifier::from_str(OID_KEY_USAGE)?, From ec342263215eea93f6f4fdd8a5b10df4c9689a72 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 20:52:18 +0200 Subject: [PATCH 136/215] Publish alpha 9 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d66add4..3cf0483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.8" +version = "0.9.0-alpha.9" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From d9d2c28d883db084daa027424f2d3ae917162380 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 22:28:40 +0200 Subject: [PATCH 137/215] Raise MSRV --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3cf0483..92194c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "polyproto" -version = "0.9.0-alpha.9" +version = "0.9.0-alpha.10" 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"] From 568cd07a180c358a6c584ed10fef3878e03e6f61 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 22:28:56 +0200 Subject: [PATCH 138/215] add cert_from_der test --- tests/certs/idcert.rs | 93 +++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/tests/certs/idcert.rs b/tests/certs/idcert.rs index 8633ca2..b215f0c 100644 --- a/tests/certs/idcert.rs +++ b/tests/certs/idcert.rs @@ -230,29 +230,78 @@ fn cert_from_pem() { assert_eq!(cert_from_pem, cert); } -#[test] -fn test_bitstrings() { +#[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 data = 255u8.to_be_bytes(); - let bitstring = BitString::new(0, data).unwrap(); - log::debug!("Bitstring: {:#?}", bitstring); - let der = bitstring.to_der().unwrap(); - let any = der::Any::from_der(&der).unwrap(); - log::debug!("Any: {:#?}", any); - log::debug!("Any as bytes: {:#?}", any.to_der().unwrap()); - log::debug!("Bitstring: {:#?}", BitString::from_der(&der).unwrap()); - let bitstring_from_any_value = BitString::from_bytes(any.value()).unwrap(); // Doesn't work! - log::debug!( - "Bitstring from Any value(): {:#?}", - bitstring_from_any_value + 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 cert = IdCert::from_actor_csr( + csr, + &priv_key, + Uint::new(&8932489u64.to_be_bytes()).unwrap(), + RdnSequence::from_str( + "CN=root,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", + ) + .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, Some(polyproto::certs::Target::Actor)).unwrap(); + log::trace!( + "Cert from pem key usages: {:#?}", + cert_from_der.id_cert_tbs.capabilities.key_usage.key_usages ); - //assert_eq!(bitstring, bitstring_from_any_value); // NOT THE SAME! - let bitstring_from_any_der = BitString::from_der(&any.to_der().unwrap()).unwrap(); - log::debug!("Bitstring from Any to_der(): {:#?}", bitstring_from_any_der); // ok - log::debug!( - "Raw bitstring from any to der: {:?}", - bitstring_from_any_der.raw_bytes() + 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, + &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 data = cert.clone().to_der().unwrap(); + let cert_from_der = + IdCert::from_der(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); + log::trace!( + "Cert from pem key usages: {:#?}", + cert_from_der.id_cert_tbs.capabilities.key_usage.key_usages ); - log::debug!("raw der bitstring {:?}", bitstring.to_der().unwrap()); - log::debug!("raw bitstring {:?}", bitstring.raw_bytes()); // Raw bytes is [255], don't use this + assert_eq!(cert_from_der, cert); } From 3b8609a9ed5fc0a953538b340a22a88b783882e7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 29 May 2024 22:29:54 +0200 Subject: [PATCH 139/215] idcsr tests: also test home server certs --- tests/certs/idcsr.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/certs/idcsr.rs b/tests/certs/idcsr.rs index 22787fa..189cd2b 100644 --- a/tests/certs/idcsr.rs +++ b/tests/certs/idcsr.rs @@ -47,8 +47,19 @@ fn csr_from_pem() { ) .unwrap(); let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let csr_from_der = IdCsr::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); - assert_eq!(csr_from_der, csr) + 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)] @@ -99,5 +110,16 @@ fn csr_from_der() { .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) + 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); } From bf7480f16cf6e1ddba2c3ff0da358e796473a934 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 30 May 2024 11:12:58 +0200 Subject: [PATCH 140/215] Add test get_server_public_key --- tests/api/core/mod.rs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 753363b..e1202e6 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -13,7 +13,10 @@ use httptest::*; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; use polyproto::certs::idcsr::IdCsr; -use polyproto::types::routes::core::v1::{GET_CHALLENGE_STRING, ROTATE_SERVER_IDENTITY_KEY}; +use polyproto::key::PublicKey; +use polyproto::types::routes::core::v1::{ + GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, +}; use polyproto::Name; use serde_json::json; use x509_cert::time::{Time, Validity}; @@ -92,3 +95,34 @@ async fn rotate_server_identity_key() { .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 + ); +} From fb2f7470460843b2477732a82e786e991d72bc8c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 30 May 2024 16:08:33 +0200 Subject: [PATCH 141/215] Add test get_server_id_cert --- tests/api/core/mod.rs | 72 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index e1202e6..4585465 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -3,11 +3,11 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::str::FromStr; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use der::asn1::{Uint, UtcTime}; -use httptest::matchers::request; use httptest::matchers::request::method_path; +use httptest::matchers::{eq, json_decoded, request}; use httptest::responders::json_encoded; use httptest::*; use polyproto::certs::capabilities::Capabilities; @@ -15,7 +15,8 @@ use polyproto::certs::idcert::IdCert; use polyproto::certs::idcsr::IdCsr; use polyproto::key::PublicKey; use polyproto::types::routes::core::v1::{ - GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, + GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, + ROTATE_SERVER_IDENTITY_KEY, }; use polyproto::Name; use serde_json::json; @@ -126,3 +127,68 @@ async fn get_server_public_key() { priv_key.public_key.public_key_info().public_key_bitstring ); } + +#[tokio::test] +async fn get_server_id_cert() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let subject = Name::from_str("CN=root,DC=polyphony,DC=chat").unwrap(); + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + let id_csr = IdCsr::::new( + &subject, + &priv_key, + &Capabilities::default_home_server(), + Some(polyproto::certs::Target::HomeServer), + ) + .unwrap(); + let id_cert = IdCert::from_ca_csr( + id_csr, + &priv_key, + Uint::new(&[8]).unwrap(), + subject, + 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_pem = id_cert.to_pem(der::pem::LineEnding::LF).unwrap(); + let server = Server::run(); + server.expect( + Expectation::matching(method_path( + GET_SERVER_PUBLIC_IDCERT.method.as_str(), + GET_SERVER_PUBLIC_IDCERT.path, + )) + .respond_with(json_encoded(json!(cert_pem))), + ); + + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let cert = client + .get_server_id_cert::(None) + .await + .unwrap(); + assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + 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": timestamp})))), + ]) + .respond_with(json_encoded(json!(cert_pem))), + ); + + let cert = client + .get_server_id_cert::(Some(timestamp)) + .await + .unwrap(); + assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); +} From f1df0eca2de2970970481155eaa95ad2b00b2d58 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 30 May 2024 23:36:31 +0200 Subject: [PATCH 142/215] Add Safety section, update rest of README --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index dafbcb0..77aff77 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,10 @@ running. things may break or change at any point in time.** 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. +[spki](https://crates.io/crates/spki). This crate exports both of these crates' types and traits +under the `der` and `spki` modules, respectively. It is also possible to use polyproto together +with the `x509_cert` crate, as all the polyproto certificate types are compatible with the +certificate types from that crate. 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 @@ -41,17 +43,26 @@ implementing polyproto by transforming the [polyproto specification](https://docs.polyphony.chat/Protocol%20Specifications/core/) into well-defined yet adaptable Rust types. -```toml -[dependencies] -polyproto = { version = "0", features = ["wasm"] } -``` +## 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 will have to use the `wasm` feature: -## reqwest +```toml +[dependencies] +polyproto = { version = "0", features = ["wasm"] } +``` + +## HTTP API client through reqwest By default, this crate uses `reqwest` for HTTP requests. `reqwest` is an optional dependency, and you can disable it by using polyproto with `default-features = false` in your `Cargo.toml`: From 15652ea5f277c69994bf9df21a08fb6c6fedfe5f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 30 May 2024 23:36:50 +0200 Subject: [PATCH 143/215] check for mismatching uid and cn --- src/constraints/name.rs | 90 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/src/constraints/name.rs b/src/constraints/name.rs index 554247c..2ceef04 100644 --- a/src/constraints/name.rs +++ b/src/constraints/name.rs @@ -18,6 +18,7 @@ impl Constrained for Name { /// - 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> { let mut num_cn: u8 = 0; let mut num_dc: u8 = 0; @@ -25,6 +26,7 @@ impl Constrained for Name { 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() { @@ -41,6 +43,7 @@ impl Constrained for Name { } OID_RDN_COMMON_NAME => { num_cn += 1; + cn = rdn.clone(); if num_cn > 1 { return Err(ConstraintError::OutOfBounds { lower: 1, @@ -62,7 +65,9 @@ impl Constrained for Name { vec_dc.reverse(); if let Some(target) = target { match target { - Target::Actor => validate_dc_matches_dc_in_uid(vec_dc, uid)?, + Target::Actor => { + validate_dc_matches_dc_in_uid(&vec_dc, &uid)?; + } Target::HomeServer => { if num_uid > 0 || num_unique_identifier > 0 { return Err(ConstraintError::OutOfBounds { @@ -76,9 +81,11 @@ impl Constrained for Name { } }; } else if num_uid != 0 { - validate_dc_matches_dc_in_uid(vec_dc, uid)?; + validate_dc_matches_dc_in_uid(&vec_dc, &uid)?; + } + if num_uid != 0 && num_cn != 0 { + validate_uid_username_matches_cn(&uid, &cn)?; } - if num_dc == 0 { return Err(ConstraintError::OutOfBounds { lower: 1, @@ -126,8 +133,8 @@ impl Constrained for Name { /// Check if the domain components are equal between the UID and the DCs fn validate_dc_matches_dc_in_uid( - vec_dc: Vec, - uid: RelativeDistinguishedName, + vec_dc: &[RelativeDistinguishedName], + uid: &RelativeDistinguishedName, ) -> Result<(), ConstraintError> { // Find the position of the @ in the UID let position_of_at = match uid.to_string().find('@') { @@ -179,6 +186,8 @@ fn validate_dc_matches_dc_in_uid( use log::trace; use x509_cert::attr::AttributeTypeAndValue; +/// 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"); @@ -192,6 +201,8 @@ fn validate_rdn_uid(item: &AttributeTypeAndValue) -> Result<(), ConstraintError> } } +/// 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> { if let Ok(value) = Ia5String::new(&String::from_utf8_lossy(item.value.value()).to_string()) { SessionId::new_validated(value)?; @@ -202,3 +213,72 @@ fn validate_rdn_unique_identifier(item: &AttributeTypeAndValue) -> Result<(), Co ))) } } + +/// 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 => { + 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 => 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() { + // TODO + } + + #[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()); + } +} From 3a58700776a625bf2067ac3343fba4474cffeb98 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 1 Jun 2024 22:46:53 +0200 Subject: [PATCH 144/215] Add public fn validate_at() for IdCert --- src/certs/idcert.rs | 7 +++++++ src/certs/idcerttbs.rs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 6c13aa5..d4b35a7 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -163,6 +163,13 @@ impl> IdCert { pub fn signature_data(&self) -> Result, ConversionError> { self.id_cert_tbs.clone().to_der() } + + /// Performs validation of the certificate. This includes checking the signature, the + /// validity period, the issuer, subject, [Capabilities] and every other constraint required + /// by the polyproto specification. + pub fn valid_at(&self, time: u64, target: Option) -> bool { + self.id_cert_tbs.valid_at(time) && self.validate(target).is_ok() + } } impl> TryFrom> for Certificate { diff --git a/src/certs/idcerttbs.rs b/src/certs/idcerttbs.rs index 9102c74..ef29eab 100644 --- a/src/certs/idcerttbs.rs +++ b/src/certs/idcerttbs.rs @@ -145,6 +145,13 @@ impl> IdCertTbs { 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() + } } impl> TryFrom> From 2ab7d6f76a18fab8ce00c0623faa690a29b66e6c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 1 Jun 2024 23:54:01 +0200 Subject: [PATCH 145/215] Change SessionId::new_validated to take &str arg instead --- src/certs/mod.rs | 14 ++++++++++++-- src/constraints/mod.rs | 15 +++------------ src/constraints/name.rs | 10 ++-------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 81c05e3..7f0564e 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -60,8 +60,18 @@ impl SessionId { /// 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 }; + pub fn new_validated(id: &str) -> Result { + let ia5string = match 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, + }; session_id.validate(None)?; Ok(session_id) } diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs index 5880d09..fd070e9 100644 --- a/src/constraints/mod.rs +++ b/src/constraints/mod.rs @@ -2,7 +2,6 @@ // 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, RelativeDistinguishedName}; @@ -192,31 +191,23 @@ mod name_constraints { #[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()) + 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( - Ia5String::new("11111111111111111111111111222222".as_bytes()).unwrap() - ) - .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( - Ia5String::new("111111111111111111111111112222223".as_bytes()).unwrap() - ) - .is_err()) + assert!(SessionId::new_validated("111111111111111111111111112222223").is_err()) } } diff --git a/src/constraints/name.rs b/src/constraints/name.rs index 2ceef04..228f8e8 100644 --- a/src/constraints/name.rs +++ b/src/constraints/name.rs @@ -204,14 +204,8 @@ fn validate_rdn_uid(item: &AttributeTypeAndValue) -> Result<(), ConstraintError> /// 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> { - if let Ok(value) = Ia5String::new(&String::from_utf8_lossy(item.value.value()).to_string()) { - SessionId::new_validated(value)?; - Ok(()) - } else { - Err(ConstraintError::Malformed(Some( - "Tried to decode SessionID (uniqueIdentifier) as Ia5String and failed".to_string(), - ))) - } + SessionId::new_validated(&String::from_utf8_lossy(item.value.value()))?; + Ok(()) } /// Validate that the UID username matches the Common Name From de43bbbfc34c6e1d0be62740bd2a9aad52eaae11 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 1 Jun 2024 23:54:34 +0200 Subject: [PATCH 146/215] Fix body creation in get_actor_id_certs --- src/api/core/mod.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 1b2b1f6..055ea5a 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -98,15 +98,25 @@ impl HttpClient { &self, fid: &str, unix_time: Option, + session_id: Option<&SessionId>, ) -> HttpResult>> { let request_url = self .url - .join(&format!("{}?{}", GET_ACTOR_IDCERTS.path, fid))?; + .join(&format!("{}{}", GET_ACTOR_IDCERTS.path, fid))?; let mut request = self .client .request(GET_ACTOR_IDCERTS.method.clone(), request_url); - if let Some(time) = unix_time { - request = request.body(json!({ "timestamp": time }).to_string()); + 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?; From 3e26406e7dc64569331a4db06916b02136a012a2 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 1 Jun 2024 23:54:48 +0200 Subject: [PATCH 147/215] Add test get_actor_id_certs --- tests/api/core/mod.rs | 182 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 2 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 4585465..ec434a9 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -7,15 +7,16 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use der::asn1::{Uint, UtcTime}; use httptest::matchers::request::method_path; -use httptest::matchers::{eq, json_decoded, request}; +use httptest::matchers::{eq, json_decoded, matches, request}; use httptest::responders::json_encoded; use httptest::*; 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::{ - GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, + GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, }; use polyproto::Name; @@ -192,3 +193,180 @@ async fn get_server_id_cert() { .unwrap(); assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); } + +#[tokio::test] +async fn get_actor_id_certs() { + init_logger(); + let mut csprng = rand::rngs::OsRng; + let subject = Name::from_str( + "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", + ) + .unwrap(); + let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + let id_csr = IdCsr::::new( + &subject, + &priv_key, + &Capabilities::default_actor(), + Some(polyproto::certs::Target::Actor), + ) + .unwrap(); + let id_certs = { + let mut vec: Vec> = Vec::new(); + for _ in 0..5 { + let cert = IdCert::from_actor_csr( + id_csr.clone(), + &priv_key, + Uint::new(&[8]).unwrap(), + subject.clone(), + 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(); + 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); +} From 27e753a059dd3ac8fc24f9ac845d04059af1e7fb Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 00:43:07 +0200 Subject: [PATCH 148/215] Provide common methods for creating certs for tests --- tests/api/core/mod.rs | 99 ++++++------------------------------------- tests/common/mod.rs | 85 ++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 87 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index ec434a9..dc310ec 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -2,28 +2,25 @@ // 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::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; -use der::asn1::{Uint, UtcTime}; use httptest::matchers::request::method_path; use httptest::matchers::{eq, json_decoded, matches, request}; use httptest::responders::json_encoded; use httptest::*; -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::{ GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, }; -use polyproto::Name; use serde_json::json; -use x509_cert::time::{Time, Validity}; -use crate::common::{init_logger, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature}; +use crate::common::{ + actor_id_cert, home_server_id_cert, init_logger, Ed25519PrivateKey, Ed25519PublicKey, + Ed25519Signature, +}; /// Correctly format the server URL for the test. fn server_url(server: &Server) -> String { @@ -55,31 +52,7 @@ async fn get_challenge_string() { async fn rotate_server_identity_key() { init_logger(); - let mut csprng = rand::rngs::OsRng; - let subject = Name::from_str("CN=root,DC=polyphony,DC=chat").unwrap(); - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - let id_csr = IdCsr::::new( - &subject, - &priv_key, - &Capabilities::default_home_server(), - Some(polyproto::certs::Target::HomeServer), - ) - .unwrap(); - let id_cert = IdCert::from_ca_csr( - id_csr, - &priv_key, - Uint::new(&[8]).unwrap(), - subject, - 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 id_cert = home_server_id_cert(); let cert_pem = id_cert.to_pem(der::pem::LineEnding::LF).unwrap(); let server = Server::run(); server.expect( @@ -132,31 +105,7 @@ async fn get_server_public_key() { #[tokio::test] async fn get_server_id_cert() { init_logger(); - let mut csprng = rand::rngs::OsRng; - let subject = Name::from_str("CN=root,DC=polyphony,DC=chat").unwrap(); - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - let id_csr = IdCsr::::new( - &subject, - &priv_key, - &Capabilities::default_home_server(), - Some(polyproto::certs::Target::HomeServer), - ) - .unwrap(); - let id_cert = IdCert::from_ca_csr( - id_csr, - &priv_key, - Uint::new(&[8]).unwrap(), - subject, - 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 id_cert = home_server_id_cert(); let cert_pem = id_cert.to_pem(der::pem::LineEnding::LF).unwrap(); let server = Server::run(); server.expect( @@ -197,37 +146,10 @@ async fn get_server_id_cert() { #[tokio::test] async fn get_actor_id_certs() { init_logger(); - let mut csprng = rand::rngs::OsRng; - let subject = Name::from_str( - "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", - ) - .unwrap(); - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); - let id_csr = IdCsr::::new( - &subject, - &priv_key, - &Capabilities::default_actor(), - Some(polyproto::certs::Target::Actor), - ) - .unwrap(); let id_certs = { let mut vec: Vec> = Vec::new(); for _ in 0..5 { - let cert = IdCert::from_actor_csr( - id_csr.clone(), - &priv_key, - Uint::new(&[8]).unwrap(), - subject.clone(), - 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 = actor_id_cert("flori"); vec.push(cert); } vec @@ -370,3 +292,8 @@ async fn get_actor_id_certs() { ); assert!(!certs[0].invalidated); } + +#[tokio::test] +async fn update_session_id_cert() { + init_logger(); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 8874730..25ab5dc 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -3,16 +3,22 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::str::FromStr; +use std::time::Duration; -use der::asn1::BitString; +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 x509_cert::time::{Time, Validity}; pub fn init_logger() { if std::env::var("RUST_LOG").is_err() { @@ -21,6 +27,83 @@ pub fn init_logger() { env_logger::builder().is_test(true).try_init().unwrap_or(()); } +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 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(&[8]).unwrap(), + actor_subject(cn), + 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() +} + +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() +} + +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(&[8]).unwrap(), + home_server_subject(), + 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() +} + +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() +} + #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct Ed25519Signature { pub(crate) signature: Ed25519DalekSignature, From 8a32afb9909c76064e50dc6cc8a9c4d71ae46e43 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 11:31:04 +0200 Subject: [PATCH 149/215] Add test update_session_id_cert --- tests/api/core/mod.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index dc310ec..ff143c9 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -6,14 +6,14 @@ use std::time::{SystemTime, UNIX_EPOCH}; use httptest::matchers::request::method_path; use httptest::matchers::{eq, json_decoded, matches, request}; -use httptest::responders::json_encoded; +use httptest::responders::{json_encoded, status_code}; use httptest::*; use polyproto::certs::idcert::IdCert; use polyproto::certs::SessionId; use polyproto::key::PublicKey; use polyproto::types::routes::core::v1::{ GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, - ROTATE_SERVER_IDENTITY_KEY, + ROTATE_SERVER_IDENTITY_KEY, UPDATE_SESSION_IDCERT, }; use serde_json::json; @@ -296,4 +296,21 @@ async fn get_actor_id_certs() { #[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(); } From 843477167722b60f0e4ab33adbc7d5aee469e588 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 13:22:20 +0200 Subject: [PATCH 150/215] Fix: session id was provided in path, not body --- src/api/core/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 055ea5a..86106a4 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -143,11 +143,11 @@ impl HttpClient { /// 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(&format!("{}{}", DELETE_SESSION.path, session_id))?; + 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(()) From b3308594f4f2251ebb731aeaeed005ccf9df959d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 13:22:29 +0200 Subject: [PATCH 151/215] Write test for delete session --- tests/api/core/mod.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index ff143c9..b0dacc8 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -12,8 +12,8 @@ use polyproto::certs::idcert::IdCert; use polyproto::certs::SessionId; use polyproto::key::PublicKey; use polyproto::types::routes::core::v1::{ - GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, - ROTATE_SERVER_IDENTITY_KEY, UPDATE_SESSION_IDCERT, + DELETE_SESSION, GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, + GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, UPDATE_SESSION_IDCERT, }; use serde_json::json; @@ -314,3 +314,25 @@ async fn update_session_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(); +} From c3665d77b9b1b2d1fac388b6e9a2d7b74376f363 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 16:18:54 +0200 Subject: [PATCH 152/215] properly format values in request body --- src/api/core/mod.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 86106a4..2f9a28b 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use serde::{Deserialize, Serialize}; -use serde_json::json; +use serde_json::{json, Value}; use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; @@ -169,12 +169,24 @@ impl HttpClient { .body(csr.to_pem(der::pem::LineEnding::LF)?) .send() .await; - let (pem, token) = - HttpClient::handle_response::<(String, String)>(request_response).await?; - Ok(( - IdCert::from_pem(pem.as_str(), Some(crate::certs::Target::Actor))?, - token, - )) + let response_value = HttpClient::handle_response::(request_response).await?; + let id_cert = if let Some(cert) = response_value.get("id_cert") { + IdCert::::from_pem(cert.as_str().unwrap(), Some(crate::certs::Target::Actor))? + } else { + return Err(crate::errors::RequestError::ConversionError( + crate::errors::InvalidInput::Malformed("Found no id_cert in response.".to_string()) + .into(), + )); + }; + let token = if let Some(token) = response_value.get("token") { + token.as_str().unwrap().to_string() + } else { + return Err(crate::errors::RequestError::ConversionError( + crate::errors::InvalidInput::Malformed("Found no token in response.".to_string()) + .into(), + )); + }; + Ok((id_cert, token)) } /// Upload encrypted private key material to the server for later retrieval. The upload size From c65cc8386e325d5e72242f3e2c08262717760836 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 16:19:03 +0200 Subject: [PATCH 153/215] add default validity function --- tests/common/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 25ab5dc..67edf2b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -35,6 +35,13 @@ pub fn actor_subject(cn: &str) -> Name { .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() } From 34e368f49622475bd0aabbe66f118364fa0f3170 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 16:20:13 +0200 Subject: [PATCH 154/215] add logging --- src/certs/idcert.rs | 12 +++++++ src/certs/idcsr.rs | 1 + src/constraints/certs.rs | 53 +++++++++++++++++++++++++--- src/constraints/name.rs | 76 ++++++++++++++++++++++++++++++++-------- 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index d4b35a7..16fd57f 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -90,7 +90,14 @@ impl> IdCert { issuer: Name, validity: Validity, ) -> Result { + log::trace!("[IdCert::from_actor_csr()] creating actor certificate"); let signature_algorithm = signing_key.algorithm_identifier(); + 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, @@ -101,11 +108,16 @@ impl> IdCert { 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, }; + log::trace!( + "[IdCert::from_actor_csr()] validating certificate with target {:?}", + Some(Target::Actor) + ); cert.validate(Some(Target::Actor))?; Ok(cert) } diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index 972e2c8..f67427a 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -83,6 +83,7 @@ impl> IdCsr { signature_algorithm, signature, }; + log::trace!("[IdCsr::new()] Validating self with Target: {:?}", target); id_csr.validate(target)?; Ok(id_csr) } diff --git a/src/constraints/certs.rs b/src/constraints/certs.rs index eb44fcc..14d08fb 100644 --- a/src/constraints/certs.rs +++ b/src/constraints/certs.rs @@ -13,7 +13,15 @@ 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 { @@ -39,16 +47,26 @@ impl> Constrained for IdCsrInner { 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(_) => return Err(ConstraintError::Malformed(Some("DER conversion failure when converting inner IdCsr to DER. IdCsr is likely malformed".to_string()))) + 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(_) => return Err(ConstraintError::Malformed(Some(ERR_MSG_SIGNATURE_MISMATCH.to_string()))) + Err(_) => { + log::warn!( + "[IdCsr::validate()] {}", ERR_MSG_SIGNATURE_MISMATCH); + return Err(ConstraintError::Malformed(Some(ERR_MSG_SIGNATURE_MISMATCH.to_string())))} }; Ok(()) } @@ -56,12 +74,19 @@ impl> Constrained for IdCsr { 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)?; + log::trace!("[IdCert::validate()] verifying signature"); match self.id_cert_tbs.subject_public_key.verify_signature( &self.signature, match &self.id_cert_tbs.clone().to_der() { Ok(data) => data, Err(_) => { + log::warn!( + "[IdCert::validate()] DER conversion failure when converting inner IdCertTbs to DER"); return Err(ConstraintError::Malformed(Some( "DER conversion failure when converting inner IdCertTbs to DER".to_string(), ))); @@ -69,18 +94,36 @@ impl> Constrained for IdCert { }, ) { Ok(_) => Ok(()), - Err(_) => Err(ConstraintError::Malformed(Some( - ERR_MSG_SIGNATURE_MISMATCH.to_string(), - ))), + Err(_) => { + log::warn!("[IdCert::validate()] {}", ERR_MSG_SIGNATURE_MISMATCH); + Err(ConstraintError::Malformed(Some( + ERR_MSG_SIGNATURE_MISMATCH.to_string(), + ))) + } } } } impl> Constrained for IdCertTbs { fn validate(&self, target: Option) -> Result<(), ConstraintError> { + log::trace!( + "[IdCertTbs::validate()] validating capabilities for target: {:?}", + target + ); self.capabilities.validate(target)?; self.issuer.validate(target)?; 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 => { diff --git a/src/constraints/name.rs b/src/constraints/name.rs index 228f8e8..cc58b3c 100644 --- a/src/constraints/name.rs +++ b/src/constraints/name.rs @@ -4,6 +4,8 @@ use crate::errors::ERR_MSG_DC_UID_MISMATCH; +use x509_cert::attr::AttributeTypeAndValue; + use super::*; impl Constrained for Name { @@ -20,6 +22,7 @@ impl Constrained for Name { /// - 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; @@ -30,18 +33,31 @@ impl Constrained for Name { 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 { @@ -49,15 +65,24 @@ impl Constrained for Name { lower: 1, upper: 1, actual: num_cn.to_string(), - reason: "Distinguished Names must not contain more than one Common Name field".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() + ); + } } } } @@ -66,6 +91,14 @@ impl Constrained for Name { 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 => { @@ -83,7 +116,13 @@ impl Constrained for Name { } 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 { @@ -140,9 +179,13 @@ fn validate_dc_matches_dc_in_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 @ @@ -152,7 +195,6 @@ fn validate_dc_matches_dc_in_uid( 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() { - trace!("Checking if component \"{}\"...", component); let equivalent_dc = match vec_dc.get(index as usize) { Some(dc) => dc, None => { @@ -162,10 +204,6 @@ fn validate_dc_matches_dc_in_uid( } }; let equivalent_dc = equivalent_dc.to_string().split_at(3).1.to_string(); - trace!( - "...is equal to component \"{}\"...", - equivalent_dc.to_string() - ); if component != &equivalent_dc.to_string() { return Err(ConstraintError::Malformed(Some( ERR_MSG_DC_UID_MISMATCH.to_string(), @@ -183,9 +221,6 @@ fn validate_dc_matches_dc_in_uid( Ok(()) } -use log::trace; -use x509_cert::attr::AttributeTypeAndValue; - /// 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> { @@ -219,18 +254,29 @@ fn validate_uid_username_matches_cn( 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 => Err(ConstraintError::Malformed(Some( - "UID username does not match the Common Name".to_string(), - ))), + 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(), + ))) + } } } From 1f9720d42884f806f925a47bb9ee55d3987d43f3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 16:20:29 +0200 Subject: [PATCH 155/215] add (unfinished) test rotate_session_id_cert --- tests/api/core/mod.rs | 55 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index b0dacc8..567cf04 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -2,24 +2,31 @@ // 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::{SystemTime, UNIX_EPOCH}; +use std::str::FromStr; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use der::asn1::{Uint, UtcTime}; 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::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_SESSION, GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, - GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, UPDATE_SESSION_IDCERT, + GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, ROTATE_SESSION_IDCERT, + UPDATE_SESSION_IDCERT, }; +use polyproto::Name; use serde_json::json; +use x509_cert::time::{Time, Validity}; use crate::common::{ - actor_id_cert, home_server_id_cert, init_logger, Ed25519PrivateKey, Ed25519PublicKey, - Ed25519Signature, + actor_csr, 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. @@ -336,3 +343,43 @@ async fn delete_session() { .await .unwrap(); } + +#[tokio::test] +async fn rotate_session_id_cert() { + init_logger(); + let actor_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(), + &gen_priv_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(); +} From 187a8e8ecf0f6f4dc052a0487435398c3a5197c7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 19:05:24 +0200 Subject: [PATCH 156/215] Major fixes (see below) - home server certs no longer use cn, uid or uniqueIdentifier in any tests. This is not yet tested for i think - Fix major oversight in how certificates were validated --- examples/ed25519_cert.rs | 5 +- examples/ed25519_from_der.rs | 5 +- src/api/core/mod.rs | 64 ++++++++++--- src/certs/idcert.rs | 168 ++++++++++++++++++++++++++++++----- src/certs/mod.rs | 16 ++-- src/constraints/certs.rs | 26 +----- src/errors/composite.rs | 14 ++- src/errors/mod.rs | 2 + tests/api/core/mod.rs | 11 +-- tests/certs/idcert.rs | 76 +++++++++------- tests/common/mod.rs | 2 +- 11 files changed, 267 insertions(+), 122 deletions(-) diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index 4a1b44c..9d4b7c0 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -69,10 +69,7 @@ fn main() { csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,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(), diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index 07a0de1..5a926e4 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -63,10 +63,7 @@ fn main() { csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,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(), diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 2f9a28b..7cffdb5 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,6 +2,8 @@ // 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 serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use x509_cert::serial_number::SerialNumber; @@ -9,7 +11,7 @@ use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; use crate::certs::{PublicKeyInfo, SessionId}; -use crate::errors::ConversionError; +use crate::errors::{ConversionError, RequestError}; use crate::key::PublicKey; use crate::signature::Signature; use crate::types::routes::core::v1::*; @@ -19,6 +21,13 @@ use super::{HttpClient, HttpResult}; // TODO: MLS routes still missing +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. @@ -34,6 +43,11 @@ impl HttpClient { /// 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> { @@ -45,14 +59,26 @@ impl HttpClient { .await; let pem = HttpClient::handle_response::(request_response).await?; log::debug!("Received IdCert: \n{}", pem); - Ok(IdCert::from_pem( - pem.as_str(), - Some(crate::certs::Target::HomeServer), - )?) + 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, @@ -66,10 +92,12 @@ impl HttpClient { } let response = request.send().await; let pem = HttpClient::handle_response::(response).await?; - Ok(IdCert::from_pem( - pem.as_str(), - Some(crate::certs::Target::HomeServer), - )?) + 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 @@ -94,6 +122,11 @@ impl HttpClient { /// 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, @@ -156,8 +189,15 @@ impl HttpClient { // Core Routes: Registration needed impl HttpClient { - /// Rotate your keys for a given session. The `session_id`` in the supplied [IdCsr] must + /// 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, @@ -171,7 +211,7 @@ impl HttpClient { .await; let response_value = HttpClient::handle_response::(request_response).await?; let id_cert = if let Some(cert) = response_value.get("id_cert") { - IdCert::::from_pem(cert.as_str().unwrap(), Some(crate::certs::Target::Actor))? + IdCert::::from_pem_unchecked(cert.to_string().as_str())? } else { return Err(crate::errors::RequestError::ConversionError( crate::errors::InvalidInput::Malformed("Found no id_cert in response.".to_string()) @@ -336,7 +376,7 @@ impl> TryFrom for IdCertExt { fn try_from(id_cert: IdCertExtJson) -> Result { Ok(Self { - id_cert: IdCert::from_pem(id_cert.id_cert.as_str(), Some(crate::certs::Target::Actor))?, + id_cert: IdCert::from_pem_unchecked(id_cert.id_cert.as_str())?, invalidated: id_cert.invalidated, }) } diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index 16fd57f..b3230f4 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -9,7 +9,7 @@ use x509_cert::name::Name; use x509_cert::time::Validity; use x509_cert::Certificate; -use crate::errors::ConversionError; +use crate::errors::{ConstraintError, ConversionError, InvalidCert, ERR_CERTIFICATE_TO_DER_ERROR}; use crate::key::{PrivateKey, PublicKey}; use crate::signature::Signature; use crate::Constrained; @@ -40,13 +40,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. + /// 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, @@ -76,13 +87,24 @@ 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_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. + /// 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, @@ -123,11 +145,30 @@ impl> IdCert { } /// Create an [IdCert] from a byte slice containing a DER encoded X.509 Certificate. - /// The resulting `IdCert` 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(value: &[u8], target: Option) -> Result { - let cert = IdCert::from_der_unchecked(value)?; - cert.validate(target)?; + /// 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) } @@ -145,17 +186,36 @@ impl> IdCert { } /// Create an [IdCert] from a byte slice containing a PEM encoded X.509 Certificate. - /// The resulting `IdCert` 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_pem(pem: &str, target: Option) -> Result { - let cert = IdCert::from_pem_unchecked(pem)?; - cert.validate(target)?; + /// 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 - /// the [Constrained] trait before using it. + /// 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) @@ -176,11 +236,73 @@ impl> IdCert { self.id_cert_tbs.clone().to_der() } - /// Performs validation of the certificate. This includes checking the signature, the - /// validity period, the issuer, subject, [Capabilities] and every other constraint required - /// by the polyproto specification. - pub fn valid_at(&self, time: u64, target: Option) -> bool { - self.id_cert_tbs.valid_at(time) && self.validate(target).is_ok() + /// 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)?) } } diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 7f0564e..b5c886f 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -160,15 +160,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/certs.rs b/src/constraints/certs.rs index 14d08fb..94b93e8 100644 --- a/src/constraints/certs.rs +++ b/src/constraints/certs.rs @@ -79,28 +79,7 @@ impl> Constrained for IdCert { target ); self.id_cert_tbs.validate(target)?; - log::trace!("[IdCert::validate()] verifying signature"); - match self.id_cert_tbs.subject_public_key.verify_signature( - &self.signature, - match &self.id_cert_tbs.clone().to_der() { - Ok(data) => data, - Err(_) => { - log::warn!( - "[IdCert::validate()] DER conversion failure when converting inner IdCertTbs to DER"); - return Err(ConstraintError::Malformed(Some( - "DER conversion failure when converting inner IdCertTbs to DER".to_string(), - ))); - } - }, - ) { - Ok(_) => Ok(()), - Err(_) => { - log::warn!("[IdCert::validate()] {}", ERR_MSG_SIGNATURE_MISMATCH); - Err(ConstraintError::Malformed(Some( - ERR_MSG_SIGNATURE_MISMATCH.to_string(), - ))) - } - } + Ok(()) } } @@ -111,7 +90,8 @@ impl> Constrained for IdCertTbs { target ); self.capabilities.validate(target)?; - self.issuer.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" diff --git a/src/errors/composite.rs b/src/errors/composite.rs index bb100f9..5592429 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -9,16 +9,12 @@ use super::base::{ConstraintError, InvalidInput}; #[derive(Error, Debug, PartialEq, Clone)] 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)] + InvalidSignature(#[from] PublicKeyError), + #[error(transparent)] + InvalidProperties(#[from] ConstraintError), #[error("The validity period of the certificate is invalid, or the certificate is expired")] InvalidValidity, - #[error("The capabilities presented on the certificate are invalid or otherwise malformed")] - InvalidCapabilities(ConstraintError), } #[derive(Error, Debug, PartialEq, Hash, Clone)] @@ -42,7 +38,7 @@ pub enum ConversionError { #[error("Critical extension cannot be converted")] UnknownCriticalExtension { oid: ObjectIdentifier }, #[error(transparent)] - IdCertError(#[from] PublicKeyError), + InvalidCert(#[from] InvalidCert), } #[cfg(feature = "reqwest")] #[derive(Error, Debug)] diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 1c87f12..cdf6214 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -14,6 +14,8 @@ 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!"; diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 567cf04..5105fc9 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -2,10 +2,9 @@ // 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::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; -use der::asn1::{Uint, UtcTime}; +use der::asn1::Uint; use httptest::matchers::request::method_path; use httptest::matchers::{eq, json_decoded, matches, request}; use httptest::responders::{json_encoded, status_code}; @@ -20,12 +19,10 @@ use polyproto::types::routes::core::v1::{ GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, ROTATE_SESSION_IDCERT, UPDATE_SESSION_IDCERT, }; -use polyproto::Name; use serde_json::json; -use x509_cert::time::{Time, Validity}; use crate::common::{ - actor_csr, actor_id_cert, actor_subject, default_validity, gen_priv_key, home_server_id_cert, + actor_id_cert, actor_subject, default_validity, gen_priv_key, home_server_id_cert, home_server_subject, init_logger, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature, }; @@ -357,7 +354,7 @@ async fn rotate_session_id_cert() { .unwrap(); let id_cert = IdCert::from_actor_csr( id_csr.clone(), - &gen_priv_key(), + &actor_signing_key, Uint::new(&[8]).unwrap(), home_server_subject(), default_validity(), diff --git a/tests/certs/idcert.rs b/tests/certs/idcert.rs index b215f0c..30dbcad 100644 --- a/tests/certs/idcert.rs +++ b/tests/certs/idcert.rs @@ -55,10 +55,7 @@ fn test_create_actor_cert() { csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,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(), @@ -135,10 +132,7 @@ fn mismatched_dcs_in_csr_and_cert() { csr, &priv_key, Uint::new(&8932489u64.to_be_bytes()).unwrap(), - RdnSequence::from_str( - "CN=root,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(), @@ -159,14 +153,15 @@ fn mismatched_dcs_in_csr_and_cert() { fn cert_from_pem() { init_logger(); let mut csprng = rand::rngs::OsRng; - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + 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, + &priv_key_actor, &Capabilities::default_actor(), Some(Target::Actor), ) @@ -174,12 +169,9 @@ fn cert_from_pem() { 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=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(), @@ -191,7 +183,13 @@ fn cert_from_pem() { ) .unwrap(); let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_pem = IdCert::from_pem(&data, Some(polyproto::certs::Target::Actor)).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 @@ -200,14 +198,14 @@ fn cert_from_pem() { let csr = polyproto::certs::idcsr::IdCsr::new( &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), - &priv_key, + &priv_key_actor, &Capabilities::default_home_server(), Some(Target::HomeServer), ) .unwrap(); let cert = IdCert::from_ca_csr( csr, - &priv_key, + &priv_key_home_server, Uint::new(&8932489u64.to_be_bytes()).unwrap(), RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), Validity { @@ -221,8 +219,13 @@ fn cert_from_pem() { ) .unwrap(); let data = cert.clone().to_pem(der::pem::LineEnding::LF).unwrap(); - let cert_from_pem = - IdCert::from_pem(&data, Some(polyproto::certs::Target::HomeServer)).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 @@ -235,14 +238,15 @@ fn cert_from_pem() { fn cert_from_der() { init_logger(); let mut csprng = rand::rngs::OsRng; - let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); + 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, + &priv_key_actor, &Capabilities::default_actor(), Some(Target::Actor), ) @@ -250,12 +254,9 @@ fn cert_from_der() { 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=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(), @@ -267,7 +268,13 @@ fn cert_from_der() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); - let cert_from_der = IdCert::from_der(&data, Some(polyproto::certs::Target::Actor)).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 @@ -276,14 +283,14 @@ fn cert_from_der() { let csr = polyproto::certs::idcsr::IdCsr::new( &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), - &priv_key, + &priv_key_actor, &Capabilities::default_home_server(), Some(Target::HomeServer), ) .unwrap(); let cert = IdCert::from_ca_csr( csr, - &priv_key, + &priv_key_home_server, Uint::new(&8932489u64.to_be_bytes()).unwrap(), RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), Validity { @@ -297,8 +304,13 @@ fn cert_from_der() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); - let cert_from_der = - IdCert::from_der(&data, Some(polyproto::certs::Target::HomeServer)).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 diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 67edf2b..3b94a76 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -56,7 +56,7 @@ pub fn actor_id_cert(cn: &str) -> IdCert { actor_csr(cn, &priv_key), &priv_key, Uint::new(&[8]).unwrap(), - actor_subject(cn), + home_server_subject(), Validity { not_before: Time::UtcTime( UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), From b965151f8e812934e84b3ab8594a69585d9183ae Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 20:18:51 +0200 Subject: [PATCH 157/215] Reflect API changes in tests --- src/api/core/mod.rs | 29 +++++++------------- tests/api/core/mod.rs | 63 ++++++++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 7cffdb5..67834a4 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -5,7 +5,7 @@ use std::time::UNIX_EPOCH; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::json; use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; @@ -209,24 +209,9 @@ impl HttpClient { .body(csr.to_pem(der::pem::LineEnding::LF)?) .send() .await; - let response_value = HttpClient::handle_response::(request_response).await?; - let id_cert = if let Some(cert) = response_value.get("id_cert") { - IdCert::::from_pem_unchecked(cert.to_string().as_str())? - } else { - return Err(crate::errors::RequestError::ConversionError( - crate::errors::InvalidInput::Malformed("Found no id_cert in response.".to_string()) - .into(), - )); - }; - let token = if let Some(token) = response_value.get("token") { - token.as_str().unwrap().to_string() - } else { - return Err(crate::errors::RequestError::ConversionError( - crate::errors::InvalidInput::Malformed("Found no token in response.".to_string()) - .into(), - )); - }; - Ok((id_cert, token)) + 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 @@ -382,6 +367,12 @@ impl> TryFrom for IdCertExt { } } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct IdCertToken { + pub id_cert: String, + pub token: String, +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 5105fc9..2f53d19 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -2,13 +2,12 @@ // 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::{SystemTime, UNIX_EPOCH}; - -use der::asn1::Uint; +use der::asn1::{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; @@ -20,6 +19,7 @@ use polyproto::types::routes::core::v1::{ UPDATE_SESSION_IDCERT, }; use serde_json::json; +use x509_cert::time::Validity; use crate::common::{ actor_id_cert, actor_subject, default_validity, gen_priv_key, home_server_id_cert, @@ -53,10 +53,39 @@ async fn get_challenge_string() { } #[tokio::test] - async fn rotate_server_identity_key() { init_logger(); - let id_cert = home_server_id_cert(); + 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( @@ -112,36 +141,19 @@ async fn get_server_id_cert() { let id_cert = home_server_id_cert(); let cert_pem = id_cert.to_pem(der::pem::LineEnding::LF).unwrap(); let server = Server::run(); - server.expect( - Expectation::matching(method_path( - GET_SERVER_PUBLIC_IDCERT.method.as_str(), - GET_SERVER_PUBLIC_IDCERT.path, - )) - .respond_with(json_encoded(json!(cert_pem))), - ); - let url = server_url(&server); let client = polyproto::api::HttpClient::new(&url).unwrap(); - let cert = client - .get_server_id_cert::(None) - .await - .unwrap(); - assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); 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": timestamp})))), + request::body(json_decoded(eq(json!({"timestamp": 10})))), ]) .respond_with(json_encoded(json!(cert_pem))), ); let cert = client - .get_server_id_cert::(Some(timestamp)) + .get_server_id_cert::(Some(10)) .await .unwrap(); assert_eq!(cert.to_pem(der::pem::LineEnding::LF).unwrap(), cert_pem); @@ -345,6 +357,7 @@ async fn delete_session() { 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, @@ -354,7 +367,7 @@ async fn rotate_session_id_cert() { .unwrap(); let id_cert = IdCert::from_actor_csr( id_csr.clone(), - &actor_signing_key, + &home_server_signing_key, Uint::new(&[8]).unwrap(), home_server_subject(), default_validity(), From 04dbe54f4f845442279c38b6ecfd20e2a13c1cf3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 2 Jun 2024 21:09:23 +0200 Subject: [PATCH 158/215] Update examples --- examples/ed25519_basic.rs | 11 +++++------ examples/ed25519_cert.rs | 16 +++++++--------- examples/ed25519_from_der.rs | 32 ++++++++++++++++++-------------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/examples/ed25519_basic.rs b/examples/ed25519_basic.rs index a642e0a..3d63779 100644 --- a/examples/ed25519_basic.rs +++ b/examples/ed25519_basic.rs @@ -19,8 +19,6 @@ 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 +58,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 @@ -216,3 +210,8 @@ impl PublicKey for Ed25519PublicKey { }) } } + +#[test] +fn test_example() { + main() +} diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index 9d4b7c0..52df44b 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -41,8 +41,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); @@ -62,8 +60,7 @@ fn main() { .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, @@ -83,13 +80,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 @@ -239,3 +232,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 5a926e4..948a9e5 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -12,7 +12,7 @@ use der::{Decode, 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; @@ -34,13 +34,15 @@ 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( @@ -48,7 +50,7 @@ fn main() { "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", ) .unwrap(), - &priv_key, + &priv_key_actor, &Capabilities::default_actor(), Some(polyproto::certs::Target::Actor), ) @@ -57,11 +59,11 @@ fn main() { 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("DC=polyphony,DC=chat").unwrap(), Validity { @@ -75,14 +77,11 @@ fn main() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); - let cert_from_der = IdCert::from_der(&data, Some(polyproto::certs::Target::Actor)).unwrap(); + let cert_from_der = + IdCert::from_der(&data, Target::Actor, 15, &priv_key_home_server.public_key).unwrap(); assert_eq!(cert_from_der, cert) } -#[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 @@ -229,3 +228,8 @@ impl PublicKey for Ed25519PublicKey { }) } } + +#[test] +fn test_example() { + main() +} From df68bca86da0ce75390d2dd4b5e49db9c4c72c30 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 13:13:16 +0200 Subject: [PATCH 159/215] Add additional documentation --- src/certs/idcert.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/certs/idcert.rs b/src/certs/idcert.rs index b3230f4..b3dec43 100644 --- a/src/certs/idcert.rs +++ b/src/certs/idcert.rs @@ -29,6 +29,15 @@ use super::Target; /// - **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 From e4404a5cdaf18a6df1b83fc6f3723c1501f33be6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 14:39:55 +0200 Subject: [PATCH 160/215] impl From for Ia5String, impl TryFrom for SessionId --- src/certs/mod.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index b5c886f..ef9c25e 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -4,9 +4,10 @@ use std::ops::{Deref, DerefMut}; -use der::asn1::{BitString, Ia5String}; +use der::asn1::BitString; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; +use ser_der::asn1::Ia5String; use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; use x509_cert::name::Name; @@ -61,7 +62,7 @@ impl SessionId { /// if needed. Checks if the input is a valid Ia5String and if the [SessionId] constraints have /// been violated. pub fn new_validated(id: &str) -> Result { - let ia5string = match Ia5String::new(id) { + let ia5string = match der::asn1::Ia5String::new(id) { Ok(string) => string, Err(_) => { return Err(ConstraintError::Malformed(Some( @@ -70,13 +71,27 @@ impl SessionId { } }; let session_id = SessionId { - session_id: ia5string, + session_id: ia5string.into(), }; session_id.validate(None)?; Ok(session_id) } } +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)] pub enum Target { Actor, From 5067fbc96b499baebf7be1c158312b83174e5822 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 14:40:27 +0200 Subject: [PATCH 161/215] fix upload_encrypted_pkm, add test for that function --- src/api/core/mod.rs | 48 ++++++------------------------------------- tests/api/core/mod.rs | 35 +++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 67834a4..14a1bf9 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -4,6 +4,7 @@ use std::time::UNIX_EPOCH; +use ser_der::asn1::Ia5String; use serde::{Deserialize, Serialize}; use serde_json::json; use x509_cert::serial_number::SerialNumber; @@ -19,8 +20,6 @@ use crate::types::ChallengeString; use super::{HttpClient, HttpResult}; -// TODO: MLS routes still missing - pub fn current_unix_time() -> u64 { std::time::SystemTime::now() .duration_since(UNIX_EPOCH) @@ -222,7 +221,7 @@ impl HttpClient { for pkm in data.iter() { body.push(json!({ "serial_number": pkm.serial_number.to_string(), - "encrypted_pkm": pkm.key_data + "key_data": pkm.key_data })); } let request_url = self.url.join(UPLOAD_ENCRYPTED_PKM.path)?; @@ -252,10 +251,10 @@ impl HttpClient { .request(GET_ENCRYPTED_PKM.method.clone(), request_url) .body(json!(body).to_string()); let response = - HttpClient::handle_response::>(request.send().await).await?; + HttpClient::handle_response::>(request.send().await).await?; let mut vec_pkm = Vec::new(); for pkm in response.into_iter() { - vec_pkm.push(EncryptedPkm::try_from(pkm)?); + vec_pkm.push(pkm); } Ok(vec_pkm) } @@ -287,49 +286,14 @@ impl HttpClient { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] /// Represents encrypted private key material. The `serial` is used to identify the key /// material. The `encrypted_pkm` is the actual encrypted private key material. pub struct EncryptedPkm { - pub serial_number: SerialNumber, + pub serial_number: Ia5String, pub key_data: String, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -/// Stringly typed version of [EncryptedPkm], used for serialization and deserialization. -/// This is necessary because [SerialNumber] from the `x509_cert` crate does not implement -/// `Serialize` and `Deserialize`. -/// -/// Implements `From` and `TryInto` for conversion between the two -/// types. -/// -/// (actually, it does not implement `TryInto`. However, [EncryptedPkm] implements -/// `TryFrom`, but you get the idea.) -pub struct EncryptedPkmJson { - pub serial_number: String, - pub key_data: String, -} - -impl From for EncryptedPkmJson { - fn from(pkm: EncryptedPkm) -> Self { - Self { - serial_number: pkm.serial_number.to_string(), - key_data: pkm.key_data, - } - } -} - -impl TryFrom for EncryptedPkm { - type Error = ConversionError; - - fn try_from(pkm: EncryptedPkmJson) -> Result { - Ok(Self { - serial_number: SerialNumber::new(pkm.serial_number.as_bytes())?, - key_data: pkm.key_data, - }) - } -} - #[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 diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 2f53d19..4cc87c4 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -7,7 +7,7 @@ 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::api::core::{current_unix_time, EncryptedPkm}; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; use polyproto::certs::idcsr::IdCsr; @@ -16,7 +16,7 @@ use polyproto::key::PublicKey; use polyproto::types::routes::core::v1::{ DELETE_SESSION, GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, ROTATE_SESSION_IDCERT, - UPDATE_SESSION_IDCERT, + UPDATE_SESSION_IDCERT, UPLOAD_ENCRYPTED_PKM, }; use serde_json::json; use x509_cert::time::Validity; @@ -393,3 +393,34 @@ async fn rotate_session_id_cert() { .await .unwrap(); } + +#[tokio::test] +async fn upload_encrypted_pkm() { + init_logger(); + let key = gen_priv_key(); + let pkm = String::from_utf8_lossy(key.key.as_bytes()).to_string(); + 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!([ + { + "key_data": pkm, + "serial_number": "one" + } + ])))) + ]) + .respond_with(status_code(201)), + ); + let url = server_url(&server); + let client = polyproto::api::HttpClient::new(&url).unwrap(); + let encrypted_pkm = EncryptedPkm { + serial_number: SessionId::new_validated("one").unwrap().into(), + key_data: pkm, + }; + client + .upload_encrypted_pkm(vec![encrypted_pkm]) + .await + .unwrap(); +} From 3494c4e38617b91f272213c787762df1d6777263 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 15:30:56 +0200 Subject: [PATCH 162/215] add TODO --- src/api/core/mod.rs | 16 ++++++++++++++++ tests/api/core/mod.rs | 3 +++ 2 files changed, 19 insertions(+) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 14a1bf9..2c0d7d4 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -213,9 +213,25 @@ impl HttpClient { Ok((id_cert, response_value.token)) } + // TODO + /* + I am thinking of creating a custom type for encrypted private key material, which contains the + following information: + - The serial number of the ID-Cert, as clear text + - A modified `SubjectPublicKeyInfo` structure, which stores the following information: + - The private key material, encrypted + - The `AlgorithmIdentifier` of the private key material + - An `AlgorithmIdentifier` for the encryption algorithm used + */ + /// 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() { diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 4cc87c4..4bd8ff1 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -424,3 +424,6 @@ async fn upload_encrypted_pkm() { .await .unwrap(); } + +#[tokio::test] +async fn get_encrypted_pkm() {} From 46b8608ee11cc8ba6071cd350e203802be90b204 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 15:38:33 +0200 Subject: [PATCH 163/215] Add conditional compiling for including serde compatible asn1 type --- src/certs/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index ef9c25e..ccf34dc 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -4,10 +4,14 @@ use std::ops::{Deref, DerefMut}; +#[cfg(not(feature = "serde"))] +use der::asn1::Ia5String; +#[cfg(feature = "serde")] +use ser_der::asn1::Ia5String; + use der::asn1::BitString; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; -use ser_der::asn1::Ia5String; use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; use x509_cert::name::Name; From 24cc030de7141f98ea5bec3a91ec5ca7f2d1f3f1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 15:38:37 +0200 Subject: [PATCH 164/215] alpha.11 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 92194c6..ddfebb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.10" +version = "0.9.0-alpha.11" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From 6c6622f257c1d83872e2726ea0d799fab420a9d5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 20:09:08 +0200 Subject: [PATCH 165/215] Remove dependency on ser_der: Integrate ser_der into polyproto --- Cargo.toml | 5 +- src/types/mod.rs | 4 ++ src/types/serde/der/ia5string.rs | 111 +++++++++++++++++++++++++++++++ src/types/serde/der/mod.rs | 7 ++ src/types/serde/mod.rs | 6 ++ 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/types/serde/der/ia5string.rs create mode 100644 src/types/serde/der/mod.rs create mode 100644 src/types/serde/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ddfebb9..cef1444 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ wasm = ["getrandom", "getrandom/js"] getrandom = ["dep:getrandom"] types = ["dep:http"] reqwest = ["dep:reqwest", "types", "serde", "dep:url"] -serde = ["dep:serde", "dep:serde_json", "dep:ser_der"] +serde = ["dep:serde", "dep:serde_json"] [dependencies] der = { version = "0.7.9", features = ["pem"] } @@ -25,10 +25,9 @@ regex = "1.10.4" 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 = "0.7.3" +spki = { version = "0.7.3", features = ["pem"] } thiserror = "1.0.59" x509-cert = "0.2.5" -ser_der = { version = "0.1.0-alpha.1", optional = true, features = ["alloc"] } log = "0.4.21" url = { version = "2.5.0", optional = true } http = { version = "1.1.0", optional = true } diff --git a/src/types/mod.rs b/src/types/mod.rs index 1a9c813..e192204 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,9 +3,13 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. pub mod challenge_string; +pub mod encrypted_pkm; pub mod federation_id; +#[cfg(feature = "serde")] +pub mod serde; pub use challenge_string::*; +pub use encrypted_pkm::*; pub use federation_id::*; pub mod routes { diff --git a/src/types/serde/der/ia5string.rs b/src/types/serde/der/ia5string.rs new file mode 100644 index 0000000..7aaf508 --- /dev/null +++ b/src/types/serde/der/ia5string.rs @@ -0,0 +1,111 @@ +// 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 mod asn1 { + use std::ops::{Deref, DerefMut}; + + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)] + pub struct Ia5String(der::asn1::Ia5String); + + impl 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 + } + } + + 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/serde/der/mod.rs b/src/types/serde/der/mod.rs new file mode 100644 index 0000000..8aff98a --- /dev/null +++ b/src/types/serde/der/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/. + +pub mod ia5string; + +pub use ia5string::*; diff --git a/src/types/serde/mod.rs b/src/types/serde/mod.rs new file mode 100644 index 0000000..2d2a94a --- /dev/null +++ b/src/types/serde/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/. + +pub mod der; +pub mod spki; From ed23c50fc66b4cc4613c1c390e65011050832ac7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 20:10:55 +0200 Subject: [PATCH 166/215] Draft up encryptedpkm struct --- src/types/encrypted_pkm.rs | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/types/encrypted_pkm.rs diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs new file mode 100644 index 0000000..c17651f --- /dev/null +++ b/src/types/encrypted_pkm.rs @@ -0,0 +1,47 @@ +// 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(feature = "serde")] +use crate::types::serde::{ + der::asn1::Ia5String, spki::SubjectPublicKeyInfo as SubjectPublicKeyInfoOwned, +}; + +#[cfg(not(feature = "serde"))] +use { + super::serde::spki::LikeSubjectPublicKeyInfo, der::asn1::Ia5String, + spki::SubjectPublicKeyInfoOwned, +}; + +use der::asn1::BitString; +use spki::AlgorithmIdentifierOwned; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// A private key material structure for storing encrypted private key material on a home server. +pub struct EncryptedPkm { + pub serial_number: Ia5String, + pub key_data: PrivateKeyInfo, + 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 { + pub algorithm: AlgorithmIdentifierOwned, + pub encrypted_private_key_bitstring: BitString, +} + +impl From for PrivateKeyInfo { + fn from(value: SubjectPublicKeyInfoOwned) -> Self { + PrivateKeyInfo { + algorithm: value.algorithm.clone(), + encrypted_private_key_bitstring: value.subject_public_key.clone(), + } + } +} + +impl From for SubjectPublicKeyInfoOwned { + fn from(value: PrivateKeyInfo) -> Self { + SubjectPublicKeyInfoOwned::new(value.algorithm, value.encrypted_private_key_bitstring) + } +} From cbeffdab064b2cd0ed295a270ccc01826a3aff35 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 20:11:17 +0200 Subject: [PATCH 167/215] Draft up encryptedpkm struct --- src/api/core/mod.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 2c0d7d4..8447c1f 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -4,7 +4,6 @@ use std::time::UNIX_EPOCH; -use ser_der::asn1::Ia5String; use serde::{Deserialize, Serialize}; use serde_json::json; use x509_cert::serial_number::SerialNumber; @@ -302,14 +301,6 @@ impl HttpClient { } } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -/// Represents encrypted private key material. The `serial` is used to identify the key -/// material. The `encrypted_pkm` is the actual encrypted private key material. -pub struct EncryptedPkm { - pub serial_number: Ia5String, - pub key_data: String, -} - #[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 From 25d25b4d11c64ea58cb6d48878080508e1fb62a8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 20:11:32 +0200 Subject: [PATCH 168/215] Remove dependency on ser_der: Integrate ser_der into polyproto --- src/certs/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index ccf34dc..79fdcc8 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -4,10 +4,10 @@ use std::ops::{Deref, DerefMut}; +#[cfg(feature = "serde")] +use crate::types::serde::der::asn1::Ia5String; #[cfg(not(feature = "serde"))] use der::asn1::Ia5String; -#[cfg(feature = "serde")] -use ser_der::asn1::Ia5String; use der::asn1::BitString; use der::pem::LineEnding; From 25fa947ffd8aebf71fa2811f98624eff88ed12dd Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 20:11:55 +0200 Subject: [PATCH 169/215] Make SubjectPublicKeyInfo serde compatible --- src/types/serde/spki/subjectpublickeyinfo.rs | 148 +++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/types/serde/spki/subjectpublickeyinfo.rs diff --git a/src/types/serde/spki/subjectpublickeyinfo.rs b/src/types/serde/spki/subjectpublickeyinfo.rs new file mode 100644 index 0000000..255725c --- /dev/null +++ b/src/types/serde/spki/subjectpublickeyinfo.rs @@ -0,0 +1,148 @@ +// 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::asn1::BitString; +use der::pem::LineEnding; +use der::{Decode, DecodePem, Encode, EncodePem}; +use serde::de::Visitor; +use serde::{Deserialize, Serialize}; +use spki::AlgorithmIdentifierOwned; + +#[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, + 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() + } + + pub fn algorithm(&self) -> &AlgorithmIdentifierOwned { + &self.0.algorithm + } + + pub fn subject_public_key(&self) -> &BitString { + &self.0.subject_public_key + } +} + +impl From for SubjectPublicKeyInfo { + fn from(spki: spki::SubjectPublicKeyInfoOwned) -> Self { + Self(spki) + } +} + +impl From for spki::SubjectPublicKeyInfoOwned { + fn from(spki: SubjectPublicKeyInfo) -> Self { + spki.0 + } +} + +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("This visitor expects 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) + } +} + +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 + } +} + +pub trait LikeSubjectPublicKeyInfo { + fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self; +} + +impl LikeSubjectPublicKeyInfo for spki::SubjectPublicKeyInfoOwned { + fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { + Self { + algorithm, + subject_public_key, + } + } +} + +impl LikeSubjectPublicKeyInfo for SubjectPublicKeyInfo { + fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { + spki::SubjectPublicKeyInfoOwned { + algorithm, + subject_public_key, + } + .into() + } +} + +// TODO: TESTS From 8a8aa44947dfc9f27d2864e4521a4c2b7c67d438 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 20:12:17 +0200 Subject: [PATCH 170/215] start working on making AlgorithmIdentifierOwned serde compatible --- .../serde/spki/algorithmidentifierowned.rs | 31 +++++++++++++++++++ src/types/serde/spki/mod.rs | 9 ++++++ 2 files changed, 40 insertions(+) create mode 100644 src/types/serde/spki/algorithmidentifierowned.rs create mode 100644 src/types/serde/spki/mod.rs diff --git a/src/types/serde/spki/algorithmidentifierowned.rs b/src/types/serde/spki/algorithmidentifierowned.rs new file mode 100644 index 0000000..c4d69e9 --- /dev/null +++ b/src/types/serde/spki/algorithmidentifierowned.rs @@ -0,0 +1,31 @@ +// 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; +use spki::ObjectIdentifier; + +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] +pub struct AlgorithmIdentifierOwned(spki::AlgorithmIdentifierOwned); + +impl AlgorithmIdentifierOwned { + pub fn new(oid: ObjectIdentifier, parameters: Option) -> Self { + Self(spki::AlgorithmIdentifierOwned { oid, parameters }) + } +} + +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 + } +} diff --git a/src/types/serde/spki/mod.rs b/src/types/serde/spki/mod.rs new file mode 100644 index 0000000..24f27bd --- /dev/null +++ b/src/types/serde/spki/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 mod algorithmidentifierowned; +pub mod subjectpublickeyinfo; + +pub use algorithmidentifierowned::*; +pub use subjectpublickeyinfo::*; From 1486e7cc4b4bfc6cd3631eccee5d2b5153122d13 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 21:21:24 +0200 Subject: [PATCH 171/215] remove period from end of expecting-phrase --- src/types/serde/der/ia5string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/serde/der/ia5string.rs b/src/types/serde/der/ia5string.rs index 7aaf508..48263df 100644 --- a/src/types/serde/der/ia5string.rs +++ b/src/types/serde/der/ia5string.rs @@ -62,7 +62,7 @@ pub mod asn1 { 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.", + "A concatenation of characters from the IA5 character set in &str format", ) } From d0bcef0d4509598016ffe6135ac47eb64af0a47e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 21:22:00 +0200 Subject: [PATCH 172/215] impl serde Deserialize and Serialize on wrapper type for AlgorithmIdentifierOwned --- .../serde/spki/algorithmidentifierowned.rs | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/types/serde/spki/algorithmidentifierowned.rs b/src/types/serde/spki/algorithmidentifierowned.rs index c4d69e9..903d4bd 100644 --- a/src/types/serde/spki/algorithmidentifierowned.rs +++ b/src/types/serde/spki/algorithmidentifierowned.rs @@ -4,7 +4,9 @@ use std::ops::{Deref, DerefMut}; -use der::Any; +use der::{Any, Decode, Encode}; +use serde::de::Visitor; +use serde::{Deserialize, Serialize}; use spki::ObjectIdentifier; #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] @@ -14,6 +16,16 @@ impl 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 { @@ -29,3 +41,60 @@ impl DerefMut for AlgorithmIdentifierOwned { &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 + } +} + +pub trait LikeAlgorithmIdentifierOwned { + fn new(oid: ObjectIdentifier, parameters: Option) -> Self; +} + +impl LikeAlgorithmIdentifierOwned for AlgorithmIdentifierOwned { + fn new(oid: ObjectIdentifier, parameters: Option) -> Self { + Self::new(oid, parameters) + } +} + +impl LikeAlgorithmIdentifierOwned for spki::AlgorithmIdentifierOwned { + fn new(oid: ObjectIdentifier, parameters: Option) -> Self { + spki::AlgorithmIdentifier { oid, parameters } + } +} + +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("this Visitor expects a DER encoded AlgorithmIdentifier with optional der::Any parameters and a BitString Key") + } +} + +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) + } +} From 066aea4debd6ae8b99b6fba72d0cba2b019401a3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 22:04:19 +0200 Subject: [PATCH 173/215] why was serde_test missing?? strange --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index cef1444..c08553e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ rand = "0.8.5" 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] From 6aa940dcef748c03cbf72572fb6010cedf4eb764 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 3 Jun 2024 22:04:58 +0200 Subject: [PATCH 174/215] rename crate::serde to serde_compat, fix code --- src/api/core/mod.rs | 2 +- src/certs/mod.rs | 3 +- src/types/encrypted_pkm.rs | 70 +++++++++++++++++-- src/types/mod.rs | 28 +++++++- .../{serde => serde_compat}/der/ia5string.rs | 0 src/types/{serde => serde_compat}/der/mod.rs | 0 src/types/{serde => serde_compat}/mod.rs | 0 .../spki/algorithmidentifierowned.rs | 8 +-- src/types/{serde => serde_compat}/spki/mod.rs | 0 .../spki/subjectpublickeyinfo.rs | 29 ++------ tests/api/core/mod.rs | 7 +- 11 files changed, 107 insertions(+), 40 deletions(-) rename src/types/{serde => serde_compat}/der/ia5string.rs (100%) rename src/types/{serde => serde_compat}/der/mod.rs (100%) rename src/types/{serde => serde_compat}/mod.rs (100%) rename src/types/{serde => serde_compat}/spki/algorithmidentifierowned.rs (96%) rename src/types/{serde => serde_compat}/spki/mod.rs (100%) rename src/types/{serde => serde_compat}/spki/subjectpublickeyinfo.rs (84%) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 8447c1f..66799af 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -15,7 +15,7 @@ use crate::errors::{ConversionError, RequestError}; use crate::key::PublicKey; use crate::signature::Signature; use crate::types::routes::core::v1::*; -use crate::types::ChallengeString; +use crate::types::{ChallengeString, EncryptedPkm}; use super::{HttpClient, HttpResult}; diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 79fdcc8..710e449 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut}; #[cfg(feature = "serde")] -use crate::types::serde::der::asn1::Ia5String; +use crate::types::serde_compat::der::asn1::Ia5String; #[cfg(not(feature = "serde"))] use der::asn1::Ia5String; @@ -74,6 +74,7 @@ impl SessionId { ))) } }; + #[allow(clippy::useless_conversion)] let session_id = SessionId { session_id: ia5string.into(), }; diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs index c17651f..9644f4e 100644 --- a/src/types/encrypted_pkm.rs +++ b/src/types/encrypted_pkm.rs @@ -3,19 +3,25 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. #[cfg(feature = "serde")] -use crate::types::serde::{ - der::asn1::Ia5String, spki::SubjectPublicKeyInfo as SubjectPublicKeyInfoOwned, +use { + crate::types::serde_compat::{ + der::asn1::Ia5String, spki::AlgorithmIdentifierOwned, + spki::SubjectPublicKeyInfo as SubjectPublicKeyInfoOwned, + }, + ::serde::Deserialize, + ::serde::Serialize, }; #[cfg(not(feature = "serde"))] use { - super::serde::spki::LikeSubjectPublicKeyInfo, der::asn1::Ia5String, - spki::SubjectPublicKeyInfoOwned, + crate::types::LikeSubjectPublicKeyInfo, + der::asn1::Ia5String, + spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}, }; use der::asn1::BitString; -use spki::AlgorithmIdentifierOwned; +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] /// A private key material structure for storing encrypted private key material on a home server. pub struct EncryptedPkm { @@ -33,8 +39,9 @@ pub struct PrivateKeyInfo { impl From for PrivateKeyInfo { fn from(value: SubjectPublicKeyInfoOwned) -> Self { + #[allow(clippy::useless_conversion)] PrivateKeyInfo { - algorithm: value.algorithm.clone(), + algorithm: value.algorithm.clone().into(), encrypted_private_key_bitstring: value.subject_public_key.clone(), } } @@ -45,3 +52,54 @@ impl From for SubjectPublicKeyInfoOwned { SubjectPublicKeyInfoOwned::new(value.algorithm, value.encrypted_private_key_bitstring) } } + +#[cfg(feature = "serde")] +mod serde { + use der::pem::LineEnding; + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + use crate::types::serde_compat::spki::SubjectPublicKeyInfo; + + struct PrivateKeyInfoVisitor; + + impl<'de> Visitor<'de> for PrivateKeyInfoVisitor { + type Value = crate::types::encrypted_pkm::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, + { + crate::types::serde_compat::spki::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/mod.rs b/src/types/mod.rs index e192204..e545c33 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -6,12 +6,38 @@ pub mod challenge_string; pub mod encrypted_pkm; pub mod federation_id; #[cfg(feature = "serde")] -pub mod serde; +pub mod serde_compat; pub use challenge_string::*; +use der::asn1::BitString; +use der::Any; pub use encrypted_pkm::*; pub use federation_id::*; +#[cfg(feature = "serde")] +use serde_compat::spki::AlgorithmIdentifierOwned; +#[cfg(not(feature = "serde"))] +use spki::AlgorithmIdentifierOwned; +use spki::ObjectIdentifier; + +pub trait LikeSubjectPublicKeyInfo { + fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self; +} + +impl LikeSubjectPublicKeyInfo for spki::SubjectPublicKeyInfoOwned { + fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { + #[allow(clippy::useless_conversion)] + Self { + algorithm: algorithm.into(), + subject_public_key, + } + } +} + +pub trait LikeAlgorithmIdentifierOwned { + fn new(oid: ObjectIdentifier, parameters: Option) -> Self; +} + pub mod routes { #[derive(Debug, Clone)] pub struct Route { diff --git a/src/types/serde/der/ia5string.rs b/src/types/serde_compat/der/ia5string.rs similarity index 100% rename from src/types/serde/der/ia5string.rs rename to src/types/serde_compat/der/ia5string.rs diff --git a/src/types/serde/der/mod.rs b/src/types/serde_compat/der/mod.rs similarity index 100% rename from src/types/serde/der/mod.rs rename to src/types/serde_compat/der/mod.rs diff --git a/src/types/serde/mod.rs b/src/types/serde_compat/mod.rs similarity index 100% rename from src/types/serde/mod.rs rename to src/types/serde_compat/mod.rs diff --git a/src/types/serde/spki/algorithmidentifierowned.rs b/src/types/serde_compat/spki/algorithmidentifierowned.rs similarity index 96% rename from src/types/serde/spki/algorithmidentifierowned.rs rename to src/types/serde_compat/spki/algorithmidentifierowned.rs index 903d4bd..8a89a64 100644 --- a/src/types/serde/spki/algorithmidentifierowned.rs +++ b/src/types/serde_compat/spki/algorithmidentifierowned.rs @@ -9,6 +9,8 @@ use serde::de::Visitor; use serde::{Deserialize, Serialize}; use spki::ObjectIdentifier; +use crate::types::LikeAlgorithmIdentifierOwned; + #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct AlgorithmIdentifierOwned(spki::AlgorithmIdentifierOwned); @@ -54,10 +56,6 @@ impl From for spki::AlgorithmIdentifierOwned { } } -pub trait LikeAlgorithmIdentifierOwned { - fn new(oid: ObjectIdentifier, parameters: Option) -> Self; -} - impl LikeAlgorithmIdentifierOwned for AlgorithmIdentifierOwned { fn new(oid: ObjectIdentifier, parameters: Option) -> Self { Self::new(oid, parameters) @@ -98,3 +96,5 @@ impl Serialize for AlgorithmIdentifierOwned { serializer.serialize_bytes(&der) } } + +// TODO: Tests diff --git a/src/types/serde/spki/mod.rs b/src/types/serde_compat/spki/mod.rs similarity index 100% rename from src/types/serde/spki/mod.rs rename to src/types/serde_compat/spki/mod.rs diff --git a/src/types/serde/spki/subjectpublickeyinfo.rs b/src/types/serde_compat/spki/subjectpublickeyinfo.rs similarity index 84% rename from src/types/serde/spki/subjectpublickeyinfo.rs rename to src/types/serde_compat/spki/subjectpublickeyinfo.rs index 255725c..aa58e3c 100644 --- a/src/types/serde/spki/subjectpublickeyinfo.rs +++ b/src/types/serde_compat/spki/subjectpublickeyinfo.rs @@ -4,12 +4,14 @@ use std::ops::{Deref, DerefMut}; +use super::super::spki::AlgorithmIdentifierOwned; use der::asn1::BitString; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; use serde::de::Visitor; use serde::{Deserialize, Serialize}; -use spki::AlgorithmIdentifierOwned; + +use crate::types::LikeSubjectPublicKeyInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct SubjectPublicKeyInfo(spki::SubjectPublicKeyInfoOwned); @@ -17,7 +19,7 @@ pub struct SubjectPublicKeyInfo(spki::SubjectPublicKeyInfoOwned); impl SubjectPublicKeyInfo { pub fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { Self(spki::SubjectPublicKeyInfoOwned { - algorithm, + algorithm: algorithm.into(), subject_public_key, }) } @@ -40,14 +42,6 @@ impl SubjectPublicKeyInfo { pub fn to_der(&self) -> Result, der::Error> { self.0.to_der() } - - pub fn algorithm(&self) -> &AlgorithmIdentifierOwned { - &self.0.algorithm - } - - pub fn subject_public_key(&self) -> &BitString { - &self.0.subject_public_key - } } impl From for SubjectPublicKeyInfo { @@ -122,23 +116,10 @@ impl DerefMut for SubjectPublicKeyInfo { } } -pub trait LikeSubjectPublicKeyInfo { - fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self; -} - -impl LikeSubjectPublicKeyInfo for spki::SubjectPublicKeyInfoOwned { - fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { - Self { - algorithm, - subject_public_key, - } - } -} - impl LikeSubjectPublicKeyInfo for SubjectPublicKeyInfo { fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { spki::SubjectPublicKeyInfoOwned { - algorithm, + algorithm: algorithm.into(), subject_public_key, } .into() diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 4bd8ff1..68c2c4c 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -7,7 +7,7 @@ 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, EncryptedPkm}; +use polyproto::api::core::current_unix_time; use polyproto::certs::capabilities::Capabilities; use polyproto::certs::idcert::IdCert; use polyproto::certs::idcsr::IdCsr; @@ -413,7 +413,8 @@ async fn upload_encrypted_pkm() { ]) .respond_with(status_code(201)), ); - let url = server_url(&server); + // TODO: Rewrite this test + /* let url = server_url(&server); let client = polyproto::api::HttpClient::new(&url).unwrap(); let encrypted_pkm = EncryptedPkm { serial_number: SessionId::new_validated("one").unwrap().into(), @@ -422,7 +423,7 @@ async fn upload_encrypted_pkm() { client .upload_encrypted_pkm(vec![encrypted_pkm]) .await - .unwrap(); + .unwrap(); */ } #[tokio::test] From 2a6c2e5a1794fd3a47dd760524e7210c19e55a49 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 13:58:51 +0200 Subject: [PATCH 175/215] Write tests to check if de-/serializing SPKIs works --- .../serde_compat/spki/subjectpublickeyinfo.rs | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/types/serde_compat/spki/subjectpublickeyinfo.rs b/src/types/serde_compat/spki/subjectpublickeyinfo.rs index aa58e3c..845cfdb 100644 --- a/src/types/serde_compat/spki/subjectpublickeyinfo.rs +++ b/src/types/serde_compat/spki/subjectpublickeyinfo.rs @@ -126,4 +126,78 @@ impl LikeSubjectPublicKeyInfo for SubjectPublicKeyInfo { } } -// TODO: TESTS +#[cfg(test)] +mod test { + use std::str::FromStr; + + use der::asn1::BitString; + use serde_json::json; + use spki::ObjectIdentifier; + + use crate::types::serde_compat::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); + } + } +} From 37f5ed021f7432ff6211fbeda787df04f7a09dc6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 13:59:02 +0200 Subject: [PATCH 176/215] comment out broken test (dw, it has a TODO attached) --- tests/api/core/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 68c2c4c..b4189e6 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -400,7 +400,7 @@ async fn upload_encrypted_pkm() { let key = gen_priv_key(); let pkm = String::from_utf8_lossy(key.key.as_bytes()).to_string(); let server = Server::run(); - server.expect( + /*server.expect( Expectation::matching(all_of![ request::method(UPLOAD_ENCRYPTED_PKM.method.to_string()), request::path(UPLOAD_ENCRYPTED_PKM.path), @@ -414,7 +414,7 @@ async fn upload_encrypted_pkm() { .respond_with(status_code(201)), ); // TODO: Rewrite this test - /* let url = server_url(&server); + let url = server_url(&server); let client = polyproto::api::HttpClient::new(&url).unwrap(); let encrypted_pkm = EncryptedPkm { serial_number: SessionId::new_validated("one").unwrap().into(), From 91a3f87db87d0d7cdbd2be5d4f9e3976ff8dd944 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 13:59:02 +0200 Subject: [PATCH 177/215] comment out broken test (dw, it has a TODO attached) --- tests/api/core/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 68c2c4c..3151133 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -396,6 +396,7 @@ async fn rotate_session_id_cert() { #[tokio::test] async fn upload_encrypted_pkm() { + /* init_logger(); let key = gen_priv_key(); let pkm = String::from_utf8_lossy(key.key.as_bytes()).to_string(); @@ -414,7 +415,7 @@ async fn upload_encrypted_pkm() { .respond_with(status_code(201)), ); // TODO: Rewrite this test - /* let url = server_url(&server); + let url = server_url(&server); let client = polyproto::api::HttpClient::new(&url).unwrap(); let encrypted_pkm = EncryptedPkm { serial_number: SessionId::new_validated("one").unwrap().into(), From 7e65148dee65d5ff6295c044c81201736254b5cb Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 16:37:14 +0200 Subject: [PATCH 178/215] Streamline usages of spki:: and der:: types which we override anyways --- src/certs/mod.rs | 6 +- src/types/der/asn1/ia5string.rs | 112 ++++++++++++++++++ src/types/der/asn1/mod.rs | 11 ++ .../der/mod.rs => der/asn1/uint.rs} | 4 - src/types/{serde_compat => der}/mod.rs | 3 +- src/types/encrypted_pkm.rs | 61 ++++++---- src/types/mod.rs | 30 +---- src/types/serde_compat/der/ia5string.rs | 111 ----------------- .../spki/algorithmidentifierowned.rs | 62 ++++------ src/types/{serde_compat => }/spki/mod.rs | 0 .../spki/subjectpublickeyinfo.rs | 108 ++++++++--------- 11 files changed, 239 insertions(+), 269 deletions(-) create mode 100644 src/types/der/asn1/ia5string.rs create mode 100644 src/types/der/asn1/mod.rs rename src/types/{serde_compat/der/mod.rs => der/asn1/uint.rs} (82%) rename src/types/{serde_compat => der}/mod.rs (88%) delete mode 100644 src/types/serde_compat/der/ia5string.rs rename src/types/{serde_compat => }/spki/algorithmidentifierowned.rs (53%) rename src/types/{serde_compat => }/spki/mod.rs (100%) rename src/types/{serde_compat => }/spki/subjectpublickeyinfo.rs (73%) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 710e449..e2c0aa5 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -4,11 +4,6 @@ use std::ops::{Deref, DerefMut}; -#[cfg(feature = "serde")] -use crate::types::serde_compat::der::asn1::Ia5String; -#[cfg(not(feature = "serde"))] -use der::asn1::Ia5String; - use der::asn1::BitString; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; @@ -16,6 +11,7 @@ use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; use x509_cert::name::Name; 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 diff --git a/src/types/der/asn1/ia5string.rs b/src/types/der/asn1/ia5string.rs new file mode 100644 index 0000000..f745bb4 --- /dev/null +++ b/src/types/der/asn1/ia5string.rs @@ -0,0 +1,112 @@ +// 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)] +pub struct Ia5String(der::asn1::Ia5String); + +impl 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..a448f0b --- /dev/null +++ b/src/types/der/asn1/mod.rs @@ -0,0 +1,11 @@ +// 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/. + +pub mod ia5string; +pub mod uint; + +pub use ia5string::*; +pub use uint::*; diff --git a/src/types/serde_compat/der/mod.rs b/src/types/der/asn1/uint.rs similarity index 82% rename from src/types/serde_compat/der/mod.rs rename to src/types/der/asn1/uint.rs index 8aff98a..7be716e 100644 --- a/src/types/serde_compat/der/mod.rs +++ b/src/types/der/asn1/uint.rs @@ -1,7 +1,3 @@ // 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 mod ia5string; - -pub use ia5string::*; diff --git a/src/types/serde_compat/mod.rs b/src/types/der/mod.rs similarity index 88% rename from src/types/serde_compat/mod.rs rename to src/types/der/mod.rs index 2d2a94a..6c06d1a 100644 --- a/src/types/serde_compat/mod.rs +++ b/src/types/der/mod.rs @@ -2,5 +2,4 @@ // 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 mod der; -pub mod spki; +pub mod asn1; diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs index 9644f4e..77bab13 100644 --- a/src/types/encrypted_pkm.rs +++ b/src/types/encrypted_pkm.rs @@ -2,28 +2,31 @@ // 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(feature = "serde")] -use { - crate::types::serde_compat::{ - der::asn1::Ia5String, spki::AlgorithmIdentifierOwned, - spki::SubjectPublicKeyInfo as SubjectPublicKeyInfoOwned, - }, - ::serde::Deserialize, - ::serde::Serialize, -}; - -#[cfg(not(feature = "serde"))] -use { - crate::types::LikeSubjectPublicKeyInfo, - der::asn1::Ia5String, - spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}, -}; - use der::asn1::BitString; -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +use super::der::asn1::Ia5String; +use super::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfo}; + +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] /// A private key material structure for storing encrypted private key material on a home server. +/// +/// JSON representation: +/// ```json +/// { +/// "serial_number": "3784567832abcdefg", +/// "key_data": "-----BEGIN[...]", +/// "encryption_algorithm": [1, 2, 840, 113549, 1, 5, 13, 1, 1, 5] +/// } +/// ``` +/// +/// where: +/// +/// - `serial_number`: [Ia5String] as a string +/// - `key_data`: [PrivateKeyInfo] as a PEM-encoded ASN.1 structure. This is just a +/// [SubjectPublicKeyInfoOwned] structure which stores an encrypted private key in the +/// `subject_public_key` field. +/// - `encryption_algorithm`: [AlgorithmIdentifierOwned], DER encoded as an array of bytes. pub struct EncryptedPkm { pub serial_number: Ia5String, pub key_data: PrivateKeyInfo, @@ -37,8 +40,8 @@ pub struct PrivateKeyInfo { pub encrypted_private_key_bitstring: BitString, } -impl From for PrivateKeyInfo { - fn from(value: SubjectPublicKeyInfoOwned) -> Self { +impl From for PrivateKeyInfo { + fn from(value: SubjectPublicKeyInfo) -> Self { #[allow(clippy::useless_conversion)] PrivateKeyInfo { algorithm: value.algorithm.clone().into(), @@ -47,24 +50,30 @@ impl From for PrivateKeyInfo { } } -impl From for SubjectPublicKeyInfoOwned { +impl From for SubjectPublicKeyInfo { fn from(value: PrivateKeyInfo) -> Self { - SubjectPublicKeyInfoOwned::new(value.algorithm, value.encrypted_private_key_bitstring) + spki::SubjectPublicKeyInfoOwned { + algorithm: value.algorithm.into(), + subject_public_key: value.encrypted_private_key_bitstring, + } + .into() } } #[cfg(feature = "serde")] -mod serde { +mod serde_support { use der::pem::LineEnding; use serde::de::Visitor; use serde::{Deserialize, Serialize}; - use crate::types::serde_compat::spki::SubjectPublicKeyInfo; + use crate::types::spki::SubjectPublicKeyInfo; + + use super::PrivateKeyInfo; struct PrivateKeyInfoVisitor; impl<'de> Visitor<'de> for PrivateKeyInfoVisitor { - type Value = crate::types::encrypted_pkm::PrivateKeyInfo; + 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") @@ -74,7 +83,7 @@ mod serde { where E: serde::de::Error, { - crate::types::serde_compat::spki::SubjectPublicKeyInfo::from_pem(v.as_bytes()) + SubjectPublicKeyInfo::from_pem(v.as_bytes()) .map_err(serde::de::Error::custom) .map(Into::into) } diff --git a/src/types/mod.rs b/src/types/mod.rs index e545c33..2fd938a 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,41 +3,15 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. pub mod challenge_string; +pub mod der; pub mod encrypted_pkm; pub mod federation_id; -#[cfg(feature = "serde")] -pub mod serde_compat; +pub mod spki; pub use challenge_string::*; -use der::asn1::BitString; -use der::Any; pub use encrypted_pkm::*; pub use federation_id::*; -#[cfg(feature = "serde")] -use serde_compat::spki::AlgorithmIdentifierOwned; -#[cfg(not(feature = "serde"))] -use spki::AlgorithmIdentifierOwned; -use spki::ObjectIdentifier; - -pub trait LikeSubjectPublicKeyInfo { - fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self; -} - -impl LikeSubjectPublicKeyInfo for spki::SubjectPublicKeyInfoOwned { - fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { - #[allow(clippy::useless_conversion)] - Self { - algorithm: algorithm.into(), - subject_public_key, - } - } -} - -pub trait LikeAlgorithmIdentifierOwned { - fn new(oid: ObjectIdentifier, parameters: Option) -> Self; -} - pub mod routes { #[derive(Debug, Clone)] pub struct Route { diff --git a/src/types/serde_compat/der/ia5string.rs b/src/types/serde_compat/der/ia5string.rs deleted file mode 100644 index 48263df..0000000 --- a/src/types/serde_compat/der/ia5string.rs +++ /dev/null @@ -1,111 +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 https://mozilla.org/MPL/2.0/. - -pub mod asn1 { - use std::ops::{Deref, DerefMut}; - - use serde::de::Visitor; - use serde::{Deserialize, Serialize}; - - #[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)] - pub struct Ia5String(der::asn1::Ia5String); - - impl 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 - } - } - - 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/serde_compat/spki/algorithmidentifierowned.rs b/src/types/spki/algorithmidentifierowned.rs similarity index 53% rename from src/types/serde_compat/spki/algorithmidentifierowned.rs rename to src/types/spki/algorithmidentifierowned.rs index 8a89a64..a44389b 100644 --- a/src/types/serde_compat/spki/algorithmidentifierowned.rs +++ b/src/types/spki/algorithmidentifierowned.rs @@ -5,12 +5,8 @@ use std::ops::{Deref, DerefMut}; use der::{Any, Decode, Encode}; -use serde::de::Visitor; -use serde::{Deserialize, Serialize}; use spki::ObjectIdentifier; -use crate::types::LikeAlgorithmIdentifierOwned; - #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct AlgorithmIdentifierOwned(spki::AlgorithmIdentifierOwned); @@ -56,44 +52,38 @@ impl From for spki::AlgorithmIdentifierOwned { } } -impl LikeAlgorithmIdentifierOwned for AlgorithmIdentifierOwned { - fn new(oid: ObjectIdentifier, parameters: Option) -> Self { - Self::new(oid, parameters) - } -} +#[cfg(feature = "serde")] +mod serde_support { + use super::AlgorithmIdentifierOwned; + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + struct AlgorithmIdentifierVisitor; -impl LikeAlgorithmIdentifierOwned for spki::AlgorithmIdentifierOwned { - fn new(oid: ObjectIdentifier, parameters: Option) -> Self { - spki::AlgorithmIdentifier { oid, parameters } - } -} + impl<'de> Visitor<'de> for AlgorithmIdentifierVisitor { + type Value = AlgorithmIdentifierOwned; -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("this Visitor expects a DER encoded AlgorithmIdentifier with optional der::Any parameters and a BitString Key") + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("this Visitor expects a DER encoded AlgorithmIdentifier with optional der::Any parameters and a BitString Key") + } } -} -impl<'de> Deserialize<'de> for AlgorithmIdentifierOwned { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_bytes(AlgorithmIdentifierVisitor) + 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) + 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) + } } } diff --git a/src/types/serde_compat/spki/mod.rs b/src/types/spki/mod.rs similarity index 100% rename from src/types/serde_compat/spki/mod.rs rename to src/types/spki/mod.rs diff --git a/src/types/serde_compat/spki/subjectpublickeyinfo.rs b/src/types/spki/subjectpublickeyinfo.rs similarity index 73% rename from src/types/serde_compat/spki/subjectpublickeyinfo.rs rename to src/types/spki/subjectpublickeyinfo.rs index 845cfdb..56e8639 100644 --- a/src/types/serde_compat/spki/subjectpublickeyinfo.rs +++ b/src/types/spki/subjectpublickeyinfo.rs @@ -8,10 +8,6 @@ use super::super::spki::AlgorithmIdentifierOwned; use der::asn1::BitString; use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; -use serde::de::Visitor; -use serde::{Deserialize, Serialize}; - -use crate::types::LikeSubjectPublicKeyInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct SubjectPublicKeyInfo(spki::SubjectPublicKeyInfoOwned); @@ -56,52 +52,6 @@ impl From for spki::SubjectPublicKeyInfoOwned { } } -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("This visitor expects 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) - } -} - impl Deref for SubjectPublicKeyInfo { type Target = spki::SubjectPublicKeyInfoOwned; @@ -116,13 +66,57 @@ impl DerefMut for SubjectPublicKeyInfo { } } -impl LikeSubjectPublicKeyInfo for SubjectPublicKeyInfo { - fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { - spki::SubjectPublicKeyInfoOwned { - algorithm: algorithm.into(), - subject_public_key, +#[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("This visitor expects 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) } - .into() } } @@ -134,7 +128,7 @@ mod test { use serde_json::json; use spki::ObjectIdentifier; - use crate::types::serde_compat::spki::AlgorithmIdentifierOwned; + use crate::types::spki::AlgorithmIdentifierOwned; use super::SubjectPublicKeyInfo; From d1d453125b032a834dc8180de1df1d4dbe8d6189 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 16:49:36 +0200 Subject: [PATCH 179/215] Document type, especially how expected input looks like for usage with serde --- src/types/spki/algorithmidentifierowned.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/types/spki/algorithmidentifierowned.rs b/src/types/spki/algorithmidentifierowned.rs index a44389b..dda4c3d 100644 --- a/src/types/spki/algorithmidentifierowned.rs +++ b/src/types/spki/algorithmidentifierowned.rs @@ -8,6 +8,16 @@ 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 { From 43021443046050eb3b3e86523fe31eabf97a976a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 16:52:27 +0200 Subject: [PATCH 180/215] correct incorrect messages when implementing serde::de::Visitor::expecting --- src/types/der/asn1/ia5string.rs | 2 +- src/types/spki/algorithmidentifierowned.rs | 2 +- src/types/spki/subjectpublickeyinfo.rs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/types/der/asn1/ia5string.rs b/src/types/der/asn1/ia5string.rs index f745bb4..5dd18f1 100644 --- a/src/types/der/asn1/ia5string.rs +++ b/src/types/der/asn1/ia5string.rs @@ -64,7 +64,7 @@ mod serde_support { 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", + "a concatenation of characters from the IA5 character set in &str format", ) } diff --git a/src/types/spki/algorithmidentifierowned.rs b/src/types/spki/algorithmidentifierowned.rs index dda4c3d..7894c9a 100644 --- a/src/types/spki/algorithmidentifierowned.rs +++ b/src/types/spki/algorithmidentifierowned.rs @@ -73,7 +73,7 @@ mod serde_support { type Value = AlgorithmIdentifierOwned; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("this Visitor expects a DER encoded AlgorithmIdentifier with optional der::Any parameters and a BitString Key") + formatter.write_str("a DER encoded AlgorithmIdentifier with optional der::Any parameters and a BitString Key") } } diff --git a/src/types/spki/subjectpublickeyinfo.rs b/src/types/spki/subjectpublickeyinfo.rs index 56e8639..23e93ce 100644 --- a/src/types/spki/subjectpublickeyinfo.rs +++ b/src/types/spki/subjectpublickeyinfo.rs @@ -79,8 +79,7 @@ mod serde_support { type Value = SubjectPublicKeyInfo; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter - .write_str("This visitor expects a valid, PEM or DER encoded SubjectPublicKeyInfo.") + formatter.write_str("a valid, PEM or DER encoded SubjectPublicKeyInfo") } fn visit_str(self, v: &str) -> Result From 71df1fc49e756d009c228bdd08956a292c3034dc Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 17:55:56 +0200 Subject: [PATCH 181/215] Add custom wrapper for der::asn1::Uint type --- src/types/der/asn1/uint.rs | 151 +++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/types/der/asn1/uint.rs b/src/types/der/asn1/uint.rs index 7be716e..aeecca4 100644 --- a/src/types/der/asn1/uint.rs +++ b/src/types/der/asn1/uint.rs @@ -1,3 +1,154 @@ // 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 crate::errors::ConversionError; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +/// Unsigned arbitrary precision `ASN.1 INTEGER` type. +/// +/// Provides heap-allocated storage for big endian bytes which comprise an unsigned integer value. +/// +/// Intended for use cases like very large integers that are used in cryptographic applications (e.g. keys, signatures). +/// +/// Wrapper around `der::asn1::Uint` to provide `From` and `TryFrom` implementations, as +/// well as serde support, if the `serde` feature is enabled. +/// +/// ## De-/Serialization value expectations +/// +/// The implementation of serde de-/serialization for this type expects the value to be a valid, unsigned 128-bit integer. +pub struct Uint(der::asn1::Uint); + +impl Uint { + pub fn new(bytes: &[u8]) -> Result { + der::asn1::Uint::new(bytes).map(Into::into) + } +} + +impl From for Uint { + fn from(u: der::asn1::Uint) -> Self { + Self(u) + } +} + +impl From for der::asn1::Uint { + fn from(u: Uint) -> Self { + u.0 + } +} + +impl From for Uint { + fn from(value: u128) -> Self { + der::asn1::Uint::new(value.to_be_bytes().as_slice()) + .unwrap() + .into() + } +} + +impl TryFrom for u128 { + type Error = ConversionError; + fn try_from(value: Uint) -> Result { + let bytes = value.0.as_bytes(); + if bytes.len() > 16 { + return Err(crate::errors::InvalidInput::Length { + min_length: 0, + 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 Deref for Uint { + type Target = der::asn1::Uint; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Uint { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(feature = "serde")] +mod serde_support { + use serde::de::Visitor; + use serde::{Deserialize, Serialize}; + + use super::Uint; + + struct UintVisitor; + + impl<'de> Visitor<'de> for UintVisitor { + type Value = Uint; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an unsigned integer of any size") + } + + fn visit_u128(self, v: u128) -> Result + where + E: serde::de::Error, + { + Ok(Uint::from(v)) + } + } + + impl<'de> Deserialize<'de> for Uint { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_u128(UintVisitor) + } + } + + impl Serialize for Uint { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer + .serialize_u128(u128::try_from(self.clone()).map_err(serde::ser::Error::custom)?) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_u128_from_uint() { + let mut val = 1u128; + while val < u128::MAX { + log::debug!("Uint from u128: {}", val); + let uint = Uint::from(val); + assert_eq!(val, u128::try_from(uint).unwrap()); + val = val.checked_mul(2).unwrap_or(u128::MAX); + } + } + + #[test] + fn test_de_serialization() { + let mut val = 1u128; + while val < u128::MAX { + log::debug!("Uint from u128: {}", val); + let uint: Uint = Uint::from(val); + let serialized = serde_json::to_string(&uint).unwrap(); + log::debug!("Serialized: {}", serialized); + let deserialized: Uint = serde_json::from_str(&serialized).unwrap(); + assert_eq!(val, u128::try_from(deserialized).unwrap()); + val = val.checked_mul(2).unwrap_or(u128::MAX); + } + } +} From d337f02a90f1376a8c436a1dab54df6b79770a68 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 17:58:06 +0200 Subject: [PATCH 182/215] Add custom wrapper for der::asn1::Uint type --- src/types/der/asn1/uint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/der/asn1/uint.rs b/src/types/der/asn1/uint.rs index aeecca4..8560192 100644 --- a/src/types/der/asn1/uint.rs +++ b/src/types/der/asn1/uint.rs @@ -18,7 +18,7 @@ use crate::errors::ConversionError; /// /// ## De-/Serialization value expectations /// -/// The implementation of serde de-/serialization for this type expects the value to be a valid, unsigned 128-bit integer. +/// The implementation of serde de-/serialization for this type expects the value to be a valid, unsigned integer, up to 128 bits. pub struct Uint(der::asn1::Uint); impl Uint { From e0f709810a5eb1d85d41f881b5034853d717380a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 18:04:07 +0200 Subject: [PATCH 183/215] correct the "expecting" message in the visitor impl --- src/types/der/asn1/uint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/der/asn1/uint.rs b/src/types/der/asn1/uint.rs index 8560192..3dc5dfa 100644 --- a/src/types/der/asn1/uint.rs +++ b/src/types/der/asn1/uint.rs @@ -92,7 +92,7 @@ mod serde_support { type Value = Uint; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an unsigned integer of any size") + formatter.write_str("an unsigned integer up to 128 bits in size") } fn visit_u128(self, v: u128) -> Result From 7362cb6e738f2304ac74201da86ae60efffb3cd5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 18:38:38 +0200 Subject: [PATCH 184/215] oops - i needed SerialNumber instead of Uint. --- src/types/der/asn1/mod.rs | 2 - src/types/der/asn1/uint.rs | 154 ---------------------------- src/types/x509_cert/mod.rs | 7 ++ src/types/x509_cert/serialnumber.rs | 92 +++++++++++++++++ 4 files changed, 99 insertions(+), 156 deletions(-) delete mode 100644 src/types/der/asn1/uint.rs create mode 100644 src/types/x509_cert/mod.rs create mode 100644 src/types/x509_cert/serialnumber.rs diff --git a/src/types/der/asn1/mod.rs b/src/types/der/asn1/mod.rs index a448f0b..7d7741c 100644 --- a/src/types/der/asn1/mod.rs +++ b/src/types/der/asn1/mod.rs @@ -5,7 +5,5 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. pub mod ia5string; -pub mod uint; pub use ia5string::*; -pub use uint::*; diff --git a/src/types/der/asn1/uint.rs b/src/types/der/asn1/uint.rs deleted file mode 100644 index 3dc5dfa..0000000 --- a/src/types/der/asn1/uint.rs +++ /dev/null @@ -1,154 +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 https://mozilla.org/MPL/2.0/. - -use std::ops::{Deref, DerefMut}; - -use crate::errors::ConversionError; - -#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -/// Unsigned arbitrary precision `ASN.1 INTEGER` type. -/// -/// Provides heap-allocated storage for big endian bytes which comprise an unsigned integer value. -/// -/// Intended for use cases like very large integers that are used in cryptographic applications (e.g. keys, signatures). -/// -/// Wrapper around `der::asn1::Uint` to provide `From` and `TryFrom` implementations, as -/// well as serde support, if the `serde` feature is enabled. -/// -/// ## De-/Serialization value expectations -/// -/// The implementation of serde de-/serialization for this type expects the value to be a valid, unsigned integer, up to 128 bits. -pub struct Uint(der::asn1::Uint); - -impl Uint { - pub fn new(bytes: &[u8]) -> Result { - der::asn1::Uint::new(bytes).map(Into::into) - } -} - -impl From for Uint { - fn from(u: der::asn1::Uint) -> Self { - Self(u) - } -} - -impl From for der::asn1::Uint { - fn from(u: Uint) -> Self { - u.0 - } -} - -impl From for Uint { - fn from(value: u128) -> Self { - der::asn1::Uint::new(value.to_be_bytes().as_slice()) - .unwrap() - .into() - } -} - -impl TryFrom for u128 { - type Error = ConversionError; - fn try_from(value: Uint) -> Result { - let bytes = value.0.as_bytes(); - if bytes.len() > 16 { - return Err(crate::errors::InvalidInput::Length { - min_length: 0, - 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 Deref for Uint { - type Target = der::asn1::Uint; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Uint { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[cfg(feature = "serde")] -mod serde_support { - use serde::de::Visitor; - use serde::{Deserialize, Serialize}; - - use super::Uint; - - struct UintVisitor; - - impl<'de> Visitor<'de> for UintVisitor { - type Value = Uint; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an unsigned integer up to 128 bits in size") - } - - fn visit_u128(self, v: u128) -> Result - where - E: serde::de::Error, - { - Ok(Uint::from(v)) - } - } - - impl<'de> Deserialize<'de> for Uint { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_u128(UintVisitor) - } - } - - impl Serialize for Uint { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer - .serialize_u128(u128::try_from(self.clone()).map_err(serde::ser::Error::custom)?) - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_u128_from_uint() { - let mut val = 1u128; - while val < u128::MAX { - log::debug!("Uint from u128: {}", val); - let uint = Uint::from(val); - assert_eq!(val, u128::try_from(uint).unwrap()); - val = val.checked_mul(2).unwrap_or(u128::MAX); - } - } - - #[test] - fn test_de_serialization() { - let mut val = 1u128; - while val < u128::MAX { - log::debug!("Uint from u128: {}", val); - let uint: Uint = Uint::from(val); - let serialized = serde_json::to_string(&uint).unwrap(); - log::debug!("Serialized: {}", serialized); - let deserialized: Uint = serde_json::from_str(&serialized).unwrap(); - assert_eq!(val, u128::try_from(deserialized).unwrap()); - val = val.checked_mul(2).unwrap_or(u128::MAX); - } - } -} diff --git a/src/types/x509_cert/mod.rs b/src/types/x509_cert/mod.rs new file mode 100644 index 0000000..bda6e19 --- /dev/null +++ b/src/types/x509_cert/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/. + +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..486228e --- /dev/null +++ b/src/types/x509_cert/serialnumber.rs @@ -0,0 +1,92 @@ +// 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, Clone, PartialEq, Eq)] +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() + } +} + +#[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) + } + } + + impl<'de> Deserialize<'de> for SerialNumber { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_bytes(SerialNumberVisitor) + } + } + + impl Serialize for SerialNumber { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.as_bytes()) + } + } +} From 565ab66ea7f26177828873bf54a768cf2c2a7f54 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 18:38:49 +0200 Subject: [PATCH 185/215] remove .to_string() --- src/api/core/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 66799af..4a8158c 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -235,7 +235,7 @@ impl HttpClient { let mut body = Vec::new(); for pkm in data.iter() { body.push(json!({ - "serial_number": pkm.serial_number.to_string(), + "serial_number": pkm.serial_number, "key_data": pkm.key_data })); } From 7c02b9487782ca5350e075d3789d6293a7e53635 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 18:39:07 +0200 Subject: [PATCH 186/215] change ia5string to be SerialNumber --- src/types/encrypted_pkm.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs index 77bab13..6b4bf64 100644 --- a/src/types/encrypted_pkm.rs +++ b/src/types/encrypted_pkm.rs @@ -4,17 +4,17 @@ use der::asn1::BitString; -use super::der::asn1::Ia5String; use super::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfo}; +use super::x509_cert::SerialNumber; #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq)] /// A private key material structure for storing encrypted private key material on a home server. /// /// JSON representation: /// ```json /// { -/// "serial_number": "3784567832abcdefg", +/// "serial_number": [41, 12, 3, 123, 4, 3, 11], /// "key_data": "-----BEGIN[...]", /// "encryption_algorithm": [1, 2, 840, 113549, 1, 5, 13, 1, 1, 5] /// } @@ -22,13 +22,13 @@ use super::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfo}; /// /// where: /// -/// - `serial_number`: [Ia5String] as a string +/// - `serial_number`: [SerialNumber], as an array of integers. Must represent a positive integer of up to 20 octets in length. /// - `key_data`: [PrivateKeyInfo] as a PEM-encoded ASN.1 structure. This is just a /// [SubjectPublicKeyInfoOwned] structure which stores an encrypted private key in the /// `subject_public_key` field. /// - `encryption_algorithm`: [AlgorithmIdentifierOwned], DER encoded as an array of bytes. pub struct EncryptedPkm { - pub serial_number: Ia5String, + pub serial_number: SerialNumber, pub key_data: PrivateKeyInfo, pub encryption_algorithm: AlgorithmIdentifierOwned, } From 6b4f48a11cdf5737c03fae6f85cbfe723f81bfaf Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 18:39:11 +0200 Subject: [PATCH 187/215] add mod x509_cert --- src/types/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mod.rs b/src/types/mod.rs index 2fd938a..349908b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,6 +7,7 @@ pub mod der; pub mod encrypted_pkm; pub mod federation_id; pub mod spki; +pub mod x509_cert; pub use challenge_string::*; pub use encrypted_pkm::*; From 9efbffcaeb517db1981d9ef1405503e81bddc2b0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 18:39:25 +0200 Subject: [PATCH 188/215] remove unneeded import --- tests/api/core/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 0a81204..577ed49 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -16,7 +16,7 @@ use polyproto::key::PublicKey; use polyproto::types::routes::core::v1::{ DELETE_SESSION, GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, GET_SERVER_PUBLIC_KEY, ROTATE_SERVER_IDENTITY_KEY, ROTATE_SESSION_IDCERT, - UPDATE_SESSION_IDCERT, UPLOAD_ENCRYPTED_PKM, + UPDATE_SESSION_IDCERT, }; use serde_json::json; use x509_cert::time::Validity; From c059035e228455f47bab95254bbcdfda881b0694 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 19:00:04 +0200 Subject: [PATCH 189/215] Replace x509_cert::serialnumber::SerialNumber with own serial number wrappertype --- src/api/core/mod.rs | 2 +- src/lib.rs | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 4a8158c..6ef52fe 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -4,9 +4,9 @@ use std::time::UNIX_EPOCH; +use crate::types::x509_cert::SerialNumber; use serde::{Deserialize, Serialize}; use serde_json::json; -use x509_cert::serial_number::SerialNumber; use crate::certs::idcert::IdCert; use crate::certs::idcsr::IdCsr; diff --git a/src/lib.rs b/src/lib.rs index abdb174..97cec13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,13 +111,8 @@ pub(crate) mod testing_utils { #[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]) { @@ -132,8 +127,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() @@ -141,8 +135,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() From 65d80d147bd13cd000cb8e9c9f706bc41c5e6679 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 19:18:49 +0200 Subject: [PATCH 190/215] Add documentation --- src/types/x509_cert/serialnumber.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/types/x509_cert/serialnumber.rs b/src/types/x509_cert/serialnumber.rs index 486228e..94ea7cb 100644 --- a/src/types/x509_cert/serialnumber.rs +++ b/src/types/x509_cert/serialnumber.rs @@ -5,6 +5,31 @@ use std::ops::{Deref, DerefMut}; #[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 { From 1d413725d121293cf44f1bc937d776bac3a7fe6a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 20:37:44 +0200 Subject: [PATCH 191/215] add de_serialization tests, add try_as_u128 --- src/types/x509_cert/serialnumber.rs | 115 +++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/src/types/x509_cert/serialnumber.rs b/src/types/x509_cert/serialnumber.rs index 94ea7cb..c7193bd 100644 --- a/src/types/x509_cert/serialnumber.rs +++ b/src/types/x509_cert/serialnumber.rs @@ -4,6 +4,10 @@ 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 @@ -71,6 +75,38 @@ impl SerialNumber { 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)) + } } #[cfg(feature = "serde")] @@ -95,6 +131,18 @@ mod serde_support { { 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 { @@ -102,7 +150,7 @@ mod serde_support { where D: serde::Deserializer<'de>, { - deserializer.deserialize_bytes(SerialNumberVisitor) + deserializer.deserialize_any(SerialNumberVisitor) } } @@ -115,3 +163,68 @@ mod serde_support { } } } + +#[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, + }; + } + } +} From 782aa03e2871229950bd97995cc2594b82df7c61 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 20:52:17 +0200 Subject: [PATCH 192/215] fix serde implementation, add test --- src/types/spki/algorithmidentifierowned.rs | 61 +++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/types/spki/algorithmidentifierowned.rs b/src/types/spki/algorithmidentifierowned.rs index 7894c9a..5bc76b5 100644 --- a/src/types/spki/algorithmidentifierowned.rs +++ b/src/types/spki/algorithmidentifierowned.rs @@ -73,7 +73,27 @@ mod serde_support { type Value = AlgorithmIdentifierOwned; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a DER encoded AlgorithmIdentifier with optional der::Any parameters and a BitString Key") + 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) } } @@ -97,4 +117,41 @@ mod serde_support { } } -// TODO: Tests +#[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); + } +} From ca46a50df8b0afd9f0f2e696d86ed633b701b013 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 22:06:11 +0200 Subject: [PATCH 193/215] move documentation about json documentation to website --- src/types/encrypted_pkm.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs index 6b4bf64..0d5fff7 100644 --- a/src/types/encrypted_pkm.rs +++ b/src/types/encrypted_pkm.rs @@ -11,22 +11,8 @@ use super::x509_cert::SerialNumber; #[derive(Debug, Clone, PartialEq, Eq)] /// A private key material structure for storing encrypted private key material on a home server. /// -/// JSON representation: -/// ```json -/// { -/// "serial_number": [41, 12, 3, 123, 4, 3, 11], -/// "key_data": "-----BEGIN[...]", -/// "encryption_algorithm": [1, 2, 840, 113549, 1, 5, 13, 1, 1, 5] -/// } -/// ``` -/// -/// where: -/// -/// - `serial_number`: [SerialNumber], as an array of integers. Must represent a positive integer of up to 20 octets in length. -/// - `key_data`: [PrivateKeyInfo] as a PEM-encoded ASN.1 structure. This is just a -/// [SubjectPublicKeyInfoOwned] structure which stores an encrypted private key in the -/// `subject_public_key` field. -/// - `encryption_algorithm`: [AlgorithmIdentifierOwned], DER encoded as an array of bytes. +/// 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 { pub serial_number: SerialNumber, pub key_data: PrivateKeyInfo, From ce61972f4d80d7e4b5bc3f67ccef38539577d6ca Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 22:08:55 +0200 Subject: [PATCH 194/215] remove unused clippy annotations --- src/certs/mod.rs | 2 +- src/types/encrypted_pkm.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index e2c0aa5..df67b40 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -70,7 +70,7 @@ impl SessionId { ))) } }; - #[allow(clippy::useless_conversion)] + let session_id = SessionId { session_id: ia5string.into(), }; diff --git a/src/types/encrypted_pkm.rs b/src/types/encrypted_pkm.rs index 0d5fff7..4eb5b0c 100644 --- a/src/types/encrypted_pkm.rs +++ b/src/types/encrypted_pkm.rs @@ -28,7 +28,6 @@ pub struct PrivateKeyInfo { impl From for PrivateKeyInfo { fn from(value: SubjectPublicKeyInfo) -> Self { - #[allow(clippy::useless_conversion)] PrivateKeyInfo { algorithm: value.algorithm.clone().into(), encrypted_private_key_bitstring: value.subject_public_key.clone(), From 6daccc026c9f9959622e4792720722cfe32b9f84 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 22:37:14 +0200 Subject: [PATCH 195/215] Add documentation to *all* types, traits and methods which were not yet documented. --- src/api/core/mod.rs | 9 +++++++ src/api/mod.rs | 3 +++ src/certs/capabilities/basic_constraints.rs | 13 +++++----- src/certs/mod.rs | 2 ++ src/constraints/mod.rs | 10 ++++---- src/constraints/types/mod.rs | 4 +-- src/errors/base.rs | 15 ++++++++++- src/errors/composite.rs | 28 ++++++++++++++++++--- src/errors/mod.rs | 2 ++ src/key.rs | 1 + src/lib.rs | 14 ++++++++++- src/types/challenge_string.rs | 4 +++ src/types/der/asn1/ia5string.rs | 14 +++++++++++ src/types/der/asn1/mod.rs | 1 + src/types/der/mod.rs | 1 + src/types/encrypted_pkm.rs | 5 ++++ src/types/federation_id.rs | 2 ++ src/types/mod.rs | 21 ++++++++++++++++ src/types/spki/algorithmidentifierowned.rs | 1 + src/types/spki/mod.rs | 2 ++ src/types/x509_cert/mod.rs | 2 ++ 21 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 6ef52fe..5dedcbd 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -19,6 +19,7 @@ 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) @@ -307,14 +308,18 @@ impl HttpClient { /// `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, } @@ -339,8 +344,12 @@ impl> TryFrom for IdCertExt { } #[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, } diff --git a/src/api/mod.rs b/src/api/mod.rs index 6483d62..50eed8b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -8,6 +8,7 @@ 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)] @@ -27,11 +28,13 @@ pub mod core; /// 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 { diff --git a/src/certs/capabilities/basic_constraints.rs b/src/certs/capabilities/basic_constraints.rs index 48c7f42..c8fd644 100644 --- a/src/certs/capabilities/basic_constraints.rs +++ b/src/certs/capabilities/basic_constraints.rs @@ -11,7 +11,7 @@ use spki::ObjectIdentifier; use x509_cert::attr::Attribute; use x509_cert::ext::Extension; -use crate::errors::{ConstraintError, InvalidInput, 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 { diff --git a/src/certs/mod.rs b/src/certs/mod.rs index df67b40..362243f 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -94,6 +94,8 @@ impl TryFrom for SessionId { } #[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, diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs index fd070e9..ffe33b4 100644 --- a/src/constraints/mod.rs +++ b/src/constraints/mod.rs @@ -19,12 +19,12 @@ use crate::{ OID_RDN_UNIQUE_IDENTIFIER, }; -pub mod capabilities; -pub mod certs; -pub mod name; -pub mod session_id; +mod capabilities; +mod certs; +mod name; +mod session_id; #[cfg(feature = "types")] -pub mod types; +mod types; #[cfg(test)] mod name_constraints { diff --git a/src/constraints/types/mod.rs b/src/constraints/types/mod.rs index 162d757..92b86ea 100644 --- a/src/constraints/types/mod.rs +++ b/src/constraints/types/mod.rs @@ -2,8 +2,8 @@ // 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 mod challenge_string; -pub mod federation_id; +mod challenge_string; +mod federation_id; use crate::certs::Target; use crate::errors::ConstraintError; 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 5592429..d18c490 100644 --- a/src/errors/composite.rs +++ b/src/errors/composite.rs @@ -8,48 +8,70 @@ 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(transparent)] - InvalidSignature(#[from] PublicKeyError), + /// 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, } -#[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)] + /// The URL could not be parsed UrlError(#[from] url::ParseError), } diff --git a/src/errors/mod.rs b/src/errors/mod.rs index cdf6214..63675d2 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -2,6 +2,8 @@ // 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 = diff --git a/src/key.rs b/src/key.rs index 1a09fd5..546cbcb 100644 --- a/src/key.rs +++ b/src/key.rs @@ -11,6 +11,7 @@ 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 97cec13..23d3c91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,9 +45,20 @@ polyproto = { version = "0", features = ["wasm"] } ``` */ +#![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; @@ -68,7 +79,7 @@ pub mod signature; /// Types used in polyproto and the polyproto HTTP/REST APIs pub mod types; -pub mod constraints; +mod constraints; pub use der; pub use spki; @@ -92,6 +103,7 @@ pub use x509_cert::name::*; /// the system. However, this makes no implications about "123" being the correct password for a /// given user account. 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>; } diff --git a/src/types/challenge_string.rs b/src/types/challenge_string.rs index ad00430..36a76fd 100644 --- a/src/types/challenge_string.rs +++ b/src/types/challenge_string.rs @@ -4,7 +4,11 @@ #[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 index 5dd18f1..442cb71 100644 --- a/src/types/der/asn1/ia5string.rs +++ b/src/types/der/asn1/ia5string.rs @@ -5,9 +5,23 @@ 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, diff --git a/src/types/der/asn1/mod.rs b/src/types/der/asn1/mod.rs index 7d7741c..e0664cc 100644 --- a/src/types/der/asn1/mod.rs +++ b/src/types/der/asn1/mod.rs @@ -4,6 +4,7 @@ // 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 index 6c06d1a..ff3ec15 100644 --- a/src/types/der/mod.rs +++ b/src/types/der/mod.rs @@ -2,4 +2,5 @@ // 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 index 4eb5b0c..8cbe2b6 100644 --- a/src/types/encrypted_pkm.rs +++ b/src/types/encrypted_pkm.rs @@ -14,15 +14,20 @@ use super::x509_cert::SerialNumber; /// 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, } diff --git a/src/types/federation_id.rs b/src/types/federation_id.rs index 3014f2e..d4c4559 100644 --- a/src/types/federation_id.rs +++ b/src/types/federation_id.rs @@ -9,9 +9,11 @@ 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, } diff --git a/src/types/mod.rs b/src/types/mod.rs index 349908b..047fdb1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,27 +2,48 @@ // 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 { diff --git a/src/types/spki/algorithmidentifierowned.rs b/src/types/spki/algorithmidentifierowned.rs index 5bc76b5..b49e3cc 100644 --- a/src/types/spki/algorithmidentifierowned.rs +++ b/src/types/spki/algorithmidentifierowned.rs @@ -21,6 +21,7 @@ use spki::ObjectIdentifier; pub struct AlgorithmIdentifierOwned(spki::AlgorithmIdentifierOwned); impl AlgorithmIdentifierOwned { + /// Create a new `AlgorithmIdentifierOwned`. pub fn new(oid: ObjectIdentifier, parameters: Option) -> Self { Self(spki::AlgorithmIdentifierOwned { oid, parameters }) } diff --git a/src/types/spki/mod.rs b/src/types/spki/mod.rs index 24f27bd..b450da1 100644 --- a/src/types/spki/mod.rs +++ b/src/types/spki/mod.rs @@ -2,6 +2,8 @@ // 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; diff --git a/src/types/x509_cert/mod.rs b/src/types/x509_cert/mod.rs index bda6e19..e9c82eb 100644 --- a/src/types/x509_cert/mod.rs +++ b/src/types/x509_cert/mod.rs @@ -2,6 +2,8 @@ // 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::*; From e7b953573d960f4ad6c5e2a20c6bbc4e5cff3deb Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 4 Jun 2024 22:44:21 +0200 Subject: [PATCH 196/215] Update documentation for the Constrained trait --- src/lib.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 23d3c91..578889c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,15 +85,24 @@ pub use der; pub use spki; pub use x509_cert::name::*; -/// Types implementing [Constrained] can be validated to be well-formed. In addition to the [Constrained] -/// trait, types can also implement [ActorConstrained] and [HomeServerConstrained] to add additional -/// guarantees about their well-formedness in the context of an actor or home server. If implemented, -/// these trait methods should be preferred over the [Constrained] trait method. +/// 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 from 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. /// From 77628a87a3fce74a8b7601869d38f7c08d0e17b4 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 10:25:30 +0200 Subject: [PATCH 197/215] add missing documentation --- src/signature.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/signature.rs b/src/signature.rs index 0599a8a..8d5c78f 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -6,6 +6,7 @@ use spki::{AlgorithmIdentifierOwned, SignatureBitStringEncoding}; /// A signature value, generated using a [SignatureAlgorithm] pub trait Signature: PartialEq + Eq + SignatureBitStringEncoding + Clone + ToString { + /// The underlying signature type type Signature; /// The signature value fn as_signature(&self) -> &Self::Signature; From 15d0d351715107cbb122721f4091a4ca3acd485d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 10:26:05 +0200 Subject: [PATCH 198/215] remove todos --- src/api/core/mod.rs | 11 ----------- src/certs/capabilities/key_usage.rs | 2 -- 2 files changed, 13 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 5dedcbd..46a3dde 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -213,17 +213,6 @@ impl HttpClient { Ok((id_cert, response_value.token)) } - // TODO - /* - I am thinking of creating a custom type for encrypted private key material, which contains the - following information: - - The serial number of the ID-Cert, as clear text - - A modified `SubjectPublicKeyInfo` structure, which stores the following information: - - The private key material, encrypted - - The `AlgorithmIdentifier` of the private key material - - An `AlgorithmIdentifier` for the encryption algorithm used - */ - /// 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. diff --git a/src/certs/capabilities/key_usage.rs b/src/certs/capabilities/key_usage.rs index 846cd13..be7e6f7 100644 --- a/src/certs/capabilities/key_usage.rs +++ b/src/certs/capabilities/key_usage.rs @@ -96,8 +96,6 @@ impl KeyUsages { // 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 }); } - // TODO: Instead of doing this, we should rather find out why the first byte is sometimes 0. - // works for now though. -bitfl0wer if byte_array[0] == 0 && byte_array.len() == 2 { byte_array.remove(0); } From dfe890687f8e55f6410131b4cc81cc6d2e55643a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 10:43:14 +0200 Subject: [PATCH 199/215] add method to_rdn_sequence to SessionId --- src/certs/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 362243f..800a11a 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -3,12 +3,13 @@ // 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; 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; @@ -77,6 +78,11 @@ impl SessionId { 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 { From 86c8f36e9ce75ebc78c2c85a1165be20e9167ae0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 10:43:24 +0200 Subject: [PATCH 200/215] update IdCsr::new documentation --- src/certs/idcsr.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index f67427a..eb96614 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -54,7 +54,9 @@ 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. /// - **capabilities**: The capabilities requested by the subject. From 576b6ccca355ea1cb5fb4bf152f39450cab62abd Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 10:43:24 +0200 Subject: [PATCH 201/215] update IdCsr::new documentation --- src/certs/idcsr.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/certs/idcsr.rs b/src/certs/idcsr.rs index f67427a..1c7fae6 100644 --- a/src/certs/idcsr.rs +++ b/src/certs/idcsr.rs @@ -41,7 +41,6 @@ pub struct IdCsr> { /// [Signature] value for the `inner_csr` pub signature: S, } -// TODO: Document that we have a SessionId struct that can be used to create valid SessionIds 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]) @@ -54,7 +53,9 @@ 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. /// - **capabilities**: The capabilities requested by the subject. From fa4890a93a6a2b51edac81f17d996983aa96fe6d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:08:10 +0200 Subject: [PATCH 202/215] false positiveeeesssss --- .vscode/ltex.hiddenFalsePositives.en-US.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/ltex.hiddenFalsePositives.en-US.txt b/.vscode/ltex.hiddenFalsePositives.en-US.txt index e823cb9..a3c565e 100644 --- a/.vscode/ltex.hiddenFalsePositives.en-US.txt +++ b/.vscode/ltex.hiddenFalsePositives.en-US.txt @@ -18,3 +18,4 @@ {"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$"} From c8fd3fe308b7b67db2c5343ec62a7b6bee1c6971 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:08:23 +0200 Subject: [PATCH 203/215] fix pkm routes --- src/api/core/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 46a3dde..21a213f 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -224,10 +224,7 @@ impl HttpClient { pub async fn upload_encrypted_pkm(&self, data: Vec) -> HttpResult<()> { let mut body = Vec::new(); for pkm in data.iter() { - body.push(json!({ - "serial_number": pkm.serial_number, - "key_data": pkm.key_data - })); + body.push(json!(pkm)); } let request_url = self.url.join(UPLOAD_ENCRYPTED_PKM.path)?; self.client @@ -249,7 +246,7 @@ impl HttpClient { let request_url = self.url.join(GET_ENCRYPTED_PKM.path)?; let mut body = Vec::new(); for serial in serials.iter() { - body.push(json!(serial.to_string())); + body.push(json!(serial.try_as_u128()?)); } let request = self .client @@ -270,7 +267,7 @@ impl HttpClient { let request_url = self.url.join(DELETE_ENCRYPTED_PKM.path)?; let mut body = Vec::new(); for serial in serials.iter() { - body.push(json!(serial.to_string())); + body.push(json!(serial.try_as_u128()?)); } self.client .request(DELETE_ENCRYPTED_PKM.method.clone(), request_url) From 8a423ac83080e33aa7a8a13cae7cabdf88f1b78d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:08:29 +0200 Subject: [PATCH 204/215] add tests for pkm routes --- tests/api/core/mod.rs | 116 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/tests/api/core/mod.rs b/tests/api/core/mod.rs index 577ed49..f4b9c53 100644 --- a/tests/api/core/mod.rs +++ b/tests/api/core/mod.rs @@ -2,7 +2,7 @@ // 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::{GeneralizedTime, Uint}; +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}; @@ -14,11 +14,16 @@ use polyproto::certs::idcsr::IdCsr; use polyproto::certs::SessionId; use polyproto::key::PublicKey; use polyproto::types::routes::core::v1::{ - DELETE_SESSION, GET_ACTOR_IDCERTS, GET_CHALLENGE_STRING, GET_SERVER_PUBLIC_IDCERT, + 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, + 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::{ @@ -394,39 +399,110 @@ async fn rotate_session_id_cert() { .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 key = gen_priv_key(); - let pkm = String::from_utf8_lossy(key.key.as_bytes()).to_string(); + 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!([ - { - "key_data": pkm, - "serial_number": "one" - } - ])))) + request::body(json_decoded(eq(json!([&encrypted_pkm])))) ]) .respond_with(status_code(201)), ); - // TODO: Rewrite this test - let url = server_url(&server); + let url = server_url(&server); let client = polyproto::api::HttpClient::new(&url).unwrap(); - let encrypted_pkm = EncryptedPkm { - serial_number: SessionId::new_validated("one").unwrap().into(), - key_data: pkm, - }; client .upload_encrypted_pkm(vec![encrypted_pkm]) .await .unwrap(); - */ } #[tokio::test] -async fn get_encrypted_pkm() {} +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); +} From 9ea0e3a79a932f19dce55653fc6c9a4f2453e884 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:12:04 +0200 Subject: [PATCH 205/215] impl TryFrom for u128, impl From for SerialNumber --- src/types/x509_cert/serialnumber.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/types/x509_cert/serialnumber.rs b/src/types/x509_cert/serialnumber.rs index c7193bd..377b9ed 100644 --- a/src/types/x509_cert/serialnumber.rs +++ b/src/types/x509_cert/serialnumber.rs @@ -109,6 +109,21 @@ impl SerialNumber { } } +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; From bc89810673b374768de93b42c0368f6b4804d261 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:20:26 +0200 Subject: [PATCH 206/215] test_dc_matches_dc_in_uid --- src/constraints/name.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/constraints/name.rs b/src/constraints/name.rs index cc58b3c..dae9e54 100644 --- a/src/constraints/name.rs +++ b/src/constraints/name.rs @@ -290,7 +290,27 @@ mod test { #[test] fn test_dc_matches_dc_in_uid() { - // TODO + 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] From 96485d89a4b2d89bd3b2932719610f7dd33d2541 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:39:58 +0200 Subject: [PATCH 207/215] update examples --- examples/ed25519_basic.rs | 5 ----- examples/ed25519_cert.rs | 7 +------ examples/ed25519_from_der.rs | 17 ++++++++--------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/examples/ed25519_basic.rs b/examples/ed25519_basic.rs index 3d63779..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,7 +15,6 @@ use polyproto::key::{PrivateKey, PublicKey}; use polyproto::signature::Signature; use rand::rngs::OsRng; use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; -use thiserror::Error; fn main() { let mut csprng = rand::rngs::OsRng; @@ -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] = { diff --git a/examples/ed25519_cert.rs b/examples/ed25519_cert.rs index 52df44b..bbcf929 100644 --- a/examples/ed25519_cert.rs +++ b/examples/ed25519_cert.rs @@ -2,12 +2,10 @@ // 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; @@ -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; -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; diff --git a/examples/ed25519_from_der.rs b/examples/ed25519_from_der.rs index 948a9e5..afac72d 100644 --- a/examples/ed25519_from_der.rs +++ b/examples/ed25519_from_der.rs @@ -2,13 +2,10 @@ // 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; @@ -17,12 +14,8 @@ 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 @@ -77,9 +70,15 @@ fn main() { ) .unwrap(); let data = cert.clone().to_der().unwrap(); + // ``::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) + 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()) } // As mentioned in the README, we start by implementing the signature trait. From 9e18854849e7c0a9ffe820bda435842b93d844c7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:40:05 +0200 Subject: [PATCH 208/215] Add http client example --- examples/http_api.rs | 205 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 examples/http_api.rs diff --git a/examples/http_api.rs b/examples/http_api.rs new file mode 100644 index 0000000..106ae54 --- /dev/null +++ b/examples/http_api.rs @@ -0,0 +1,205 @@ +// 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::str::FromStr; + +use der::asn1::BitString; +use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; +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::certs::PublicKeyInfo; +use polyproto::key::{PrivateKey, PublicKey}; +use polyproto::signature::Signature; +use polyproto::types::routes::core::v1::GET_CHALLENGE_STRING; +use rand::rngs::OsRng; +use serde_json::json; +use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; + +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); +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct Ed25519Signature { + signature: Ed25519DalekSignature, + 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. + type Signature = Ed25519DalekSignature; + + // This is straightforward: we return a reference to the signature. + fn as_signature(&self) -> &Self::Signature { + &self.signature + } + + // The algorithm identifier for a given signature implementation is constant. We just need + // to define it here. + fn algorithm_identifier() -> AlgorithmIdentifierOwned { + AlgorithmIdentifierOwned { + // This is the OID for Ed25519. It is defined in the IANA registry. + oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), + // For this example, we don't need or want any parameters. + parameters: None, + } + } + + fn from_bytes(signature: &[u8]) -> Self { + let mut signature_vec = signature.to_vec(); + signature_vec.resize(64, 0); + let signature_array: [u8; 64] = { + let mut array = [0; 64]; + array.copy_from_slice(&signature_vec[..]); + array + }; + Self { + signature: Ed25519DalekSignature::from_bytes(&signature_array), + algorithm: Self::algorithm_identifier(), + } + } +} + +// The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement +// it for our signature type. +impl SignatureBitStringEncoding for Ed25519Signature { + fn to_bitstring(&self) -> der::Result { + BitString::from_bytes(&self.as_signature().to_bytes()) + } +} + +// Next, we implement the key traits. We start by defining the private key type. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Ed25519PrivateKey { + // Defined below + public_key: Ed25519PublicKey, + // The private key from the ed25519-dalek crate + key: SigningKey, +} + +impl PrivateKey for Ed25519PrivateKey { + type PublicKey = Ed25519PublicKey; + + // Return a reference to the public key + fn pubkey(&self) -> &Self::PublicKey { + &self.public_key + } + + // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can + // harness all of its functionality, such as the `sign` method. + fn sign(&self, data: &[u8]) -> Ed25519Signature { + let signature = self.key.sign(data); + Ed25519Signature { + signature, + algorithm: self.algorithm_identifier(), + } + } +} + +impl Ed25519PrivateKey { + // Let's also define a handy method to generate a key pair. + pub fn gen_keypair(csprng: &mut OsRng) -> Self { + let key = SigningKey::generate(csprng); + let public_key = Ed25519PublicKey { + key: key.verifying_key(), + }; + Self { public_key, key } + } +} + +// Same thing as above for the public key type. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Ed25519PublicKey { + // The public key type from the ed25519-dalek crate + key: VerifyingKey, +} + +impl PublicKey for Ed25519PublicKey { + // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. + // This method is used to mitigate weak key forgery. + fn verify_signature( + &self, + signature: &Ed25519Signature, + data: &[u8], + ) -> Result<(), polyproto::errors::composite::PublicKeyError> { + match self.key.verify_strict(data, signature.as_signature()) { + Ok(_) => Ok(()), + Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), + } + } + + // Returns the public key info. Public key info is used to encode the public key in a + // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 + // standard, and thus includes the information needed to encode the public key in a certificate + // or a CSR. + fn public_key_info(&self) -> PublicKeyInfo { + PublicKeyInfo { + algorithm: Ed25519Signature::algorithm_identifier(), + public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), + } + } + + fn try_from_public_key_info( + public_key_info: PublicKeyInfo, + ) -> std::result::Result { + let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); + key_vec.resize(32, 0); + let signature_array: [u8; 32] = { + let mut array = [0; 32]; + array.copy_from_slice(&key_vec[..]); + array + }; + Ok(Self { + key: VerifyingKey::from_bytes(&signature_array).unwrap(), + }) + } +} + +#[test] +fn test_example() {} From 38c2672c630038a61f828cb57c799d26e543d04e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:41:15 +0200 Subject: [PATCH 209/215] remove unneeded code from example --- examples/http_api.rs | 152 ------------------------------------------- 1 file changed, 152 deletions(-) diff --git a/examples/http_api.rs b/examples/http_api.rs index 106ae54..569dd77 100644 --- a/examples/http_api.rs +++ b/examples/http_api.rs @@ -2,23 +2,14 @@ // 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::str::FromStr; - -use der::asn1::BitString; -use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; 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::certs::PublicKeyInfo; -use polyproto::key::{PrivateKey, PublicKey}; -use polyproto::signature::Signature; use polyproto::types::routes::core::v1::GET_CHALLENGE_STRING; -use rand::rngs::OsRng; use serde_json::json; -use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; async fn setup_example() -> Server { let server = Server::run(); @@ -58,148 +49,5 @@ async fn main() { println!("Challenge expires at UNIX timestamp: {}", challenge.expires); } -#[derive(Debug, PartialEq, Eq, Clone)] -struct Ed25519Signature { - signature: Ed25519DalekSignature, - 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. - type Signature = Ed25519DalekSignature; - - // This is straightforward: we return a reference to the signature. - fn as_signature(&self) -> &Self::Signature { - &self.signature - } - - // The algorithm identifier for a given signature implementation is constant. We just need - // to define it here. - fn algorithm_identifier() -> AlgorithmIdentifierOwned { - AlgorithmIdentifierOwned { - // This is the OID for Ed25519. It is defined in the IANA registry. - oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), - // For this example, we don't need or want any parameters. - parameters: None, - } - } - - fn from_bytes(signature: &[u8]) -> Self { - let mut signature_vec = signature.to_vec(); - signature_vec.resize(64, 0); - let signature_array: [u8; 64] = { - let mut array = [0; 64]; - array.copy_from_slice(&signature_vec[..]); - array - }; - Self { - signature: Ed25519DalekSignature::from_bytes(&signature_array), - algorithm: Self::algorithm_identifier(), - } - } -} - -// The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement -// it for our signature type. -impl SignatureBitStringEncoding for Ed25519Signature { - fn to_bitstring(&self) -> der::Result { - BitString::from_bytes(&self.as_signature().to_bytes()) - } -} - -// Next, we implement the key traits. We start by defining the private key type. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PrivateKey { - // Defined below - public_key: Ed25519PublicKey, - // The private key from the ed25519-dalek crate - key: SigningKey, -} - -impl PrivateKey for Ed25519PrivateKey { - type PublicKey = Ed25519PublicKey; - - // Return a reference to the public key - fn pubkey(&self) -> &Self::PublicKey { - &self.public_key - } - - // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can - // harness all of its functionality, such as the `sign` method. - fn sign(&self, data: &[u8]) -> Ed25519Signature { - let signature = self.key.sign(data); - Ed25519Signature { - signature, - algorithm: self.algorithm_identifier(), - } - } -} - -impl Ed25519PrivateKey { - // Let's also define a handy method to generate a key pair. - pub fn gen_keypair(csprng: &mut OsRng) -> Self { - let key = SigningKey::generate(csprng); - let public_key = Ed25519PublicKey { - key: key.verifying_key(), - }; - Self { public_key, key } - } -} - -// Same thing as above for the public key type. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Ed25519PublicKey { - // The public key type from the ed25519-dalek crate - key: VerifyingKey, -} - -impl PublicKey for Ed25519PublicKey { - // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. - // This method is used to mitigate weak key forgery. - fn verify_signature( - &self, - signature: &Ed25519Signature, - data: &[u8], - ) -> Result<(), polyproto::errors::composite::PublicKeyError> { - match self.key.verify_strict(data, signature.as_signature()) { - Ok(_) => Ok(()), - Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), - } - } - - // Returns the public key info. Public key info is used to encode the public key in a - // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 - // standard, and thus includes the information needed to encode the public key in a certificate - // or a CSR. - fn public_key_info(&self) -> PublicKeyInfo { - PublicKeyInfo { - algorithm: Ed25519Signature::algorithm_identifier(), - public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), - } - } - - fn try_from_public_key_info( - public_key_info: PublicKeyInfo, - ) -> std::result::Result { - let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); - key_vec.resize(32, 0); - let signature_array: [u8; 32] = { - let mut array = [0; 32]; - array.copy_from_slice(&key_vec[..]); - array - }; - Ok(Self { - key: VerifyingKey::from_bytes(&signature_array).unwrap(), - }) - } -} - #[test] fn test_example() {} From 3b430f69dca63191cfbed74c36e17a73a3ca0577 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:41:53 +0200 Subject: [PATCH 210/215] publish beta.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c08553e..10040cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0-alpha.11" +version = "0.9.0-beta.1" edition = "2021" license = "MPL-2.0" description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" From 55505a60ce987c002faca6d9739f48a22b2370f1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:53:02 +0200 Subject: [PATCH 211/215] Update README --- .vscode/ltex.hiddenFalsePositives.en-US.txt | 1 + README.md | 38 ++++++------- src/lib.rs | 60 ++++++++++++++++++--- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/.vscode/ltex.hiddenFalsePositives.en-US.txt b/.vscode/ltex.hiddenFalsePositives.en-US.txt index a3c565e..6616a09 100644 --- a/.vscode/ltex.hiddenFalsePositives.en-US.txt +++ b/.vscode/ltex.hiddenFalsePositives.en-US.txt @@ -19,3 +19,4 @@ {"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/README.md b/README.md index 77aff77..456b496 100644 --- a/README.md +++ b/README.md @@ -12,18 +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). This crate exports both of these crates' types and traits -under the `der` and `spki` modules, respectively. It is also possible to use polyproto together -with the `x509_cert` crate, as all the polyproto certificate types are compatible with the -certificate types from that crate. +## 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 @@ -33,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 @@ -62,17 +62,17 @@ will have to use the `wasm` feature: polyproto = { version = "0", features = ["wasm"] } ``` -## HTTP API client through reqwest +## HTTP API client through `reqwest` -By default, this crate uses `reqwest` for HTTP requests. `reqwest` is an optional dependency, and -you can disable it by using polyproto with `default-features = false` in your `Cargo.toml`: +If the `reqwest` feature is activated, this crate offers a polyproto HTTP API client, using the +`reqwest` crate. -```toml -[dependencies] -polyproto = { version = "0", default-features = false, features = ["routes"] } -``` +### Alternatives to `reqwest` -Disabling `reqwest` gives you the ability to use your own HTTP client. +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 diff --git a/src/lib.rs b/src/lib.rs index 578889c..e9b74a8 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/. /*! +

# 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 @@ -43,6 +66,27 @@ will have to use the `wasm` feature: [dependencies] 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)] From 07d87d9668ab9d0d5f947e7c98d405b2cede8911 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 12:53:33 +0200 Subject: [PATCH 212/215] Fix grammatical error --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e9b74a8..d51c01d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,7 +139,7 @@ pub use x509_cert::name::*; /// /// 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 from not doing so. +/// 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, From 417fa59832e1e94278d1c70ee9748b1b46c08297 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 13:10:52 +0200 Subject: [PATCH 213/215] change macos runner to ubuntu runner because gecko is unsupported on macos --- .github/workflows/build_and_test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index cc987b0..19b77b1 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -31,7 +31,7 @@ jobs: cargo test --verbose --all-features --tests --examples fi wasm: - runs-on: macos-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 @@ -47,8 +47,6 @@ jobs: 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 From 8c22fbd459812098c802570930374c55494fa4ca Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 13:10:59 +0200 Subject: [PATCH 214/215] fix Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 390f829..872bdb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polyproto" -version = "0.9.0 +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" From a993b10490c4998a8447bf008fc4d236d46c3240 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 5 Jun 2024 13:16:05 +0200 Subject: [PATCH 215/215] boot out wasm tests for now --- .github/workflows/build_and_test.yml | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 19b77b1..e3f028d 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -30,23 +30,23 @@ jobs: cargo build --verbose --all-features cargo test --verbose --all-features --tests --examples fi - 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 + # 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