├── .github ├── license-check │ ├── config.json │ └── header-mpl-2.0.txt └── workflows │ ├── build_and_test.yml │ ├── coverage.yml │ ├── license-header.yml │ └── wasm.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── examples ├── ed25519_basic.rs ├── ed25519_cert.rs ├── ed25519_from_der.rs └── http_api.rs ├── gen-coverage.sh ├── headaches.md ├── scripts ├── check-license-header.sh ├── fmt.sh ├── gen-coverage.sh ├── test.sh └── wasm-pack-helper ├── src ├── api │ ├── cacheable_cert.rs │ ├── core │ │ ├── federated_identity.rs │ │ ├── migration.rs │ │ ├── mod.rs │ │ ├── rawr.rs │ │ └── services.rs │ └── mod.rs ├── certs │ ├── capabilities │ │ ├── basic_constraints.rs │ │ ├── key_usage.rs │ │ └── mod.rs │ ├── idcert.rs │ ├── idcerttbs.rs │ ├── idcsr.rs │ └── mod.rs ├── constraints │ ├── capabilities.rs │ ├── certs.rs │ ├── mod.rs │ ├── name.rs │ ├── session_id.rs │ └── types │ │ ├── federation_id.rs │ │ ├── mod.rs │ │ ├── rawr.rs │ │ └── service.rs ├── errors │ ├── base.rs │ ├── composite.rs │ └── mod.rs ├── gateway │ ├── backends │ │ ├── heartbeat.rs │ │ ├── mod.rs │ │ ├── tungstenite │ │ │ └── mod.rs │ │ └── wasm │ │ │ └── mod.rs │ └── mod.rs ├── key.rs ├── lib.rs ├── signature.rs ├── types │ ├── der │ │ ├── asn1 │ │ │ ├── ia5string.rs │ │ │ ├── mod.rs │ │ │ └── uint.rs │ │ └── mod.rs │ ├── encrypted_pkm.rs │ ├── federation_id.rs │ ├── gateway │ │ ├── mod.rs │ │ └── payload.rs │ ├── keytrial.rs │ ├── mod.rs │ ├── p2_export.rs │ ├── rawr.rs │ ├── service.rs │ ├── spki │ │ ├── algorithmidentifierowned.rs │ │ ├── mod.rs │ │ └── subjectpublickeyinfo.rs │ └── x509_cert │ │ ├── mod.rs │ │ └── serialnumber.rs └── wasm_bindgen │ ├── errors.rs │ ├── mod.rs │ ├── types.rs │ └── utils.rs ├── tests ├── api │ ├── core │ │ └── mod.rs │ └── mod.rs ├── certs │ ├── capabilities │ │ ├── key_usage.rs │ │ └── mod.rs │ ├── idcert.rs │ ├── idcsr.rs │ └── mod.rs ├── common │ └── mod.rs ├── gateway │ └── mod.rs ├── mod.rs └── types │ ├── cacheable_cert.rs │ ├── federation_id.rs │ ├── gateway.rs │ ├── keys.rs │ └── mod.rs └── third_party └── tungstenite ├── LICENSE-APACHE └── LICENSE-MIT /.github/license-check/config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "include": ["**/*.rs"], 4 | "license": "./.github/license-check/header-mpl-2.0.txt", 5 | "exclude": ["target/**", "**/"] 6 | }, 7 | { 8 | "include": ["target/**", "**/**"] 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /.github/license-check/header-mpl-2.0.txt: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test (nextest) 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | linux: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install rust toolchain 1.85.1 19 | uses: dtolnay/rust-toolchain@1.85.1 20 | - uses: Swatinem/rust-cache@v2 21 | with: 22 | cache-all-crates: "true" 23 | prefix-key: "linux" 24 | - uses: taiki-e/install-action@nextest 25 | - name: nextest run 26 | run: | 27 | cargo nextest run --features="types,reqwest,gateway" --failure-output final --no-fail-fast 28 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Publish Coverage 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | linux: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: Swatinem/rust-cache@v2 18 | with: 19 | cache-all-crates: "true" 20 | prefix-key: "coverage" 21 | - name: Build, Publish Coverage 22 | run: | 23 | if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then 24 | curl -L --proto '=https' --tlsv1.3 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 25 | cargo binstall --no-confirm cargo-tarpaulin --force 26 | cargo tarpaulin --features="gateway, reqwest, serde, types" --tests --verbose --skip-clean --coveralls ${{ secrets.COVERALLS_REPO_TOKEN }} --timeout 120 --fail-immediately 27 | else 28 | echo "Code Coverage step is skipped on PRs from forks." 29 | fi 30 | -------------------------------------------------------------------------------- /.github/workflows/license-header.yml: -------------------------------------------------------------------------------- 1 | name: License Header Check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check-license-headers: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Check license headers 13 | uses: viperproject/check-license-header@v2 14 | with: 15 | path: . 16 | config: ./.github/license-check/config.json 17 | strict: true 18 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test (wasm32) 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | wasm: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 15 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install rust toolchain 1.85.1 19 | uses: dtolnay/rust-toolchain@1.85.1 20 | - uses: Swatinem/rust-cache@v2 21 | with: 22 | cache-all-crates: "true" 23 | prefix-key: "wasm" 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: "lts/*" 27 | check-latest: true 28 | - name: Compile wasm 29 | run: | 30 | rustup target add wasm32-unknown-unknown 31 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 32 | cargo binstall --no-confirm wasm-pack --force 33 | wasm-pack build --features=wasm,serde,types --no-default-features 34 | wasm-pack build --features=wasm,types --no-default-features 35 | wasm-pack build --features=wasm --no-default-features 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .idea 4 | .DS_Store 5 | .VSCodeCounter 6 | .coverage 7 | cert.csr 8 | cert.der 9 | /firedbg 10 | .vscode/ltex.* 11 | pkg/ 12 | wasm-pack.log -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["markis.code-coverage"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markiscodecoverage.searchCriteria": ".coverage/lcov*.info", 3 | // --- Toggle the below setting using line comments to enable/disable working with wasm platforms --- 4 | "rust-analyzer.cargo.features": ["types", "reqwest", "gateway", "serde"] 5 | // --- Toggle the below settings using line comments to enable/disable working with wasm platforms --- 6 | // "rust-analyzer.cargo.features": [ 7 | // "_wasm_bindgen", 8 | // "wasm", 9 | // "types", 10 | // "reqwest", 11 | // "gateway", 12 | // "serde" 13 | // ], 14 | // "rust-analyzer.cargo.noDefaultFeatures": true, 15 | // "rust-analyzer.cargo.target": "wasm32-unknown-unknown" 16 | } 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "polyproto" 3 | version = "0.10.0" 4 | edition = "2024" 5 | license = "MPL-2.0" 6 | description = "(Generic) Rust types and traits to quickly get a polyproto implementation up and running" 7 | repository = "https://github.com/polyphony-chat/polyproto" 8 | rust-version = "1.85.0" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [features] 14 | default = ["types", "serde", "gateway", "tokio/net"] 15 | wasm = ["getrandom", "getrandom/js", "dep:ws_stream_wasm"] 16 | getrandom = ["dep:getrandom"] 17 | types = ["dep:http"] 18 | reqwest = ["dep:reqwest", "types", "serde"] 19 | serde = ["dep:serde", "serde_json", "serde_with", "url/serde"] 20 | serde_with = ["dep:serde_with"] 21 | serde_json = ["dep:serde_json"] 22 | gateway = ["serde", "types"] 23 | _wasm_bindgen = ["wasm", "dep:wasm-bindgen", "dep:js-sys", "dep:wee_alloc"] 24 | __no_wee_alloc = [] 25 | 26 | [dependencies] 27 | der = { version = "0.7.9", features = ["pem"] } 28 | getrandom = { version = "0.2.15", optional = true } 29 | regex = "1.11.1" 30 | reqwest = { version = "0.12.15", features = [ 31 | "json", 32 | "zstd", 33 | "rustls-tls-webpki-roots", 34 | "charset", 35 | "http2", 36 | "macos-system-configuration", 37 | "multipart", 38 | ], optional = true, default-features = false } 39 | serde = { version = "1.0.219", optional = true, features = ["derive"] } 40 | serde_json = { version = "1.0.140", optional = true } 41 | spki = { version = "0.7.3", features = ["pem"] } 42 | thiserror = "2.0.12" 43 | x509-cert = "0.2.5" 44 | log = "0.4.27" 45 | url = { version = "2.5.4" } 46 | http = { version = "1.3.1", optional = true } 47 | serde_with = { version = "3.12.0", optional = true } 48 | hex = "0.4.3" 49 | tokio = { version = "1.44.1", features = ["macros", "sync", "time", "rt"] } 50 | webpki-roots = "0.26.8" 51 | futures-util = "0.3.31" 52 | urlencoding = "2.1.3" 53 | ws_stream_wasm = { version = "0.7.4", optional = true } 54 | 55 | [target.'cfg(target_arch = "wasm32")'.dependencies] 56 | wasm-bindgen = { version = "0.2.100", optional = true } 57 | js-sys = { version = "0.3.77", optional = true } 58 | wee_alloc = { version = "0.4.5", optional = true } 59 | 60 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 61 | rustls = "0.23.25" 62 | tokio-tungstenite = { version = "0.26.2", features = [ 63 | "rustls-tls-webpki-roots", 64 | "url", 65 | ] } 66 | 67 | [dev-dependencies] 68 | ed25519-dalek = { version = "2.1.1", features = ["rand_core", "signature"] } 69 | rand = "0.8.5" 70 | serde = { version = "1.0.219", features = ["derive"] } 71 | serde_json = { version = "1.0.140" } 72 | serde_test = "1.0.177" 73 | polyproto = { path = "./", features = ["types", "reqwest", "serde"] } 74 | env_logger = "0.11.7" 75 | 76 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 77 | ws-mock = "0.3.0" 78 | httptest = "0.16.3" 79 | 80 | 81 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 82 | wasm-bindgen-test = "0.3.50" 83 | # The `console_error_panic_hook` crate provides better debugging of panics by 84 | # logging them with `console.error`. This is great for development, but requires 85 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 86 | # code size when deploying. 87 | console_error_panic_hook = { version = "0.1.7" } 88 | 89 | [target.'cfg(target_arch = "wasm32")'.release] 90 | # Tell `rustc` to optimize for small code size. 91 | opt-level = "s" 92 | lto = true 93 | codegen-units = 1 94 | panic = "abort" 95 | 96 | [lints.rust] 97 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | a purple cog, split in the middle along the horizontal axis with a gap inbetween the two halves. three overlayed, offset sinus-like waves travel through that gap. each wave has a different shade of purple 2 | 3 | ### `polyproto` 4 | 5 | ![dev-status] 6 | [![Discord]][Discord-invite] 7 | [![Build][build-shield]][build-url] 8 | [![Coverage][coverage-shield]][coverage-url] 9 | 10 | Crate supplying (generic) Rust types and traits to quickly get a 11 | [polyproto](https://docs.polyphony.chat/Protocol%20Specifications/core/) implementation up and 12 | running, as well as an HTTP client for the polyproto API. 13 | 14 | **[Overview/TL;DR][overview]** • **[crates.io][crates-link]** • **[Protocol Specification][docs]** 15 | 16 | ## Crate overview 17 | 18 | Building upon types offered by the [der](https://crates.io/crates/der), 19 | [x509_cert](https://crates.io/crates/x509_cert) and [spki](https://crates.io/crates/spki) crates, 20 | this crate provides a set of types and traits to quickly implement the polyproto specification. 21 | Simply add cryptography and signature algorithm crates of your choice to the mix, and you are ready 22 | to go. 23 | 24 | All polyproto certificate types can be converted to and from the types offered by the `x509_cert` 25 | crate. 26 | 27 | ## Implementing polyproto 28 | 29 | Start by implementing the trait `[crate::signature::Signature]` for a signature algorithm of your 30 | choice. Popular crates for cryptography and signature algorithms supply their own `PublicKey` and 31 | `PrivateKey` types. You should extend upon these types with your own structs and implement the 32 | `[crate::key]` traits for these new structs. 33 | 34 | You can then use the `[crate::certs]` types to build certificates using your implementations of the 35 | aforementioned traits. 36 | 37 | **View the [examples](./examples/)** directory for a simple example on how to implement and use this 38 | crate with the ED25519 signature algorithm. 39 | 40 | ## Cryptography 41 | 42 | This crate provides no cryptographic functionality whatsoever; its sole purpose is to aid in 43 | implementing polyproto by transforming the 44 | [polyproto specification](https://docs.polyphony.chat/Protocol%20Specifications/core/) into 45 | well-defined yet adaptable Rust types. 46 | 47 | ## Safety 48 | 49 | Please refer to the documentation of individual functions for information on which safety guarantees 50 | they provide. Methods returning certificates, certificate requests and other types where the 51 | validity and correctness of the data has a chance of impacting the security of a system always 52 | mention the safety guarantees they provide in their respective documentation. 53 | 54 | This crate has not undergone any security audits. 55 | 56 | ## WebAssembly 57 | 58 | This crate is designed to work with the `wasm32-unknown-unknown` target. To compile for `wasm`, you 59 | will have to use the `wasm` feature: 60 | 61 | ```toml 62 | [dependencies] 63 | polyproto = { version = "0", features = ["wasm"] } 64 | ``` 65 | 66 | Additionally, you will have to compile the project using the `--no-default-features` flag, to ensure 67 | that `tokio/net` is not pulled in as a feature dependency. The `types`, `serde`, `reqwest` and `gateway` 68 | features all work with WASM. 69 | 70 | ## HTTP API client through `reqwest` 71 | 72 | If the `reqwest` feature is activated, this crate offers a polyproto HTTP API client, using the 73 | `reqwest` crate. 74 | 75 | ### Alternatives to `reqwest` 76 | 77 | If you would like to implement an HTTP client using something other than `reqwest`, simply enable 78 | the `types` and `serde` features. Using these features, you can implement your own HTTP client, with 79 | the polyproto crate acting as a single source of truth for request and response types, as well as 80 | request routes and methods through the exported `static` `Route`s. 81 | 82 | ## WebSocket Gateway client 83 | 84 | Since `v0.10`, this crate ships polyproto WebSocket Gateway client functionality, gated behind the `gateway` feature. 85 | The implementation of this feature is super backend-agnostic—though, for now, we have sealed the needed traits, and are only shipping a `tokio-tungstenite` backend for testing. 86 | 87 | The gateway handles establishing a connection to the server, sending regular heartbeats at the specified interval and responding to Opcode 11—the manual heartbeat request. 88 | 89 | Apart from the Hello payload, library consumers can easily get access to all messages received from the gateway by calling `subscribe()` on the internal `tokio::sync::watch::Sender`. This means that this crate handles only the bare necessities of connecting to the gateway, and that you are free to handle incoming messages however you would like to. Our `GatewayMessage` type is `.into()` and `From::<>`-compatible with `tokio_tungstenite::tungstenite::Message`, so that you are not locked into using our message types, should you not want that. 90 | 91 | ## Versioning and MSRV 92 | 93 | Semver v2.0 is used for the versioning scheme for this crate. 94 | 95 | The default feature set of this crate is used to determine, verify and update the MSRV and semver version 96 | of this crate. 97 | 98 | ## Logo 99 | 100 | The polyproto logo was designed by the wonderful [antidoxi](https://antidoxi.carrd.co/). 101 | The polyproto logos provided in this document are not covered by the MPL-2.0 license covering the rest 102 | of this project. 103 | 104 | [dev-status]: https://img.shields.io/static/v1?label=Status&message=Alpha&color=blue 105 | [build-shield]: https://img.shields.io/github/actions/workflow/status/polyphony-chat/polyproto-rs/build_and_test.yml?style=flat 106 | [build-url]: https://github.com/polyphony-chat/polyproto-rs/blob/main/.github/workflows/build_and_test.yml 107 | [coverage-shield]: https://coveralls.io/repos/github/polyphony-chat/polyproto-rs/badge.svg?branch=main 108 | [coverage-url]: https://coveralls.io/github/polyphony-chat/polyproto-rs?branch=main 109 | [Discord]: https://dcbadge.vercel.app/api/server/m3FpcapGDD?style=flat 110 | [Discord-invite]: https://discord.com/invite/m3FpcapGDD 111 | [crates-link]: https://crates.io/crates/polyproto 112 | [docs]: https://docs.polyphony.chat/Protocol%20Specifications/core/ 113 | [overview]: https://docs.polyphony.chat/Overviews/core/ 114 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please write an E-Mail to [info@polyphony.chat](mailto:info@polyphony.chat). We take reports seriously and will reply to you as soon as possible. 6 | -------------------------------------------------------------------------------- /examples/ed25519_basic.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | // Example implementation of polyproto's signature and key traits for ed25519-dalek. 6 | // This example is not complete and should not be copy-pasted into a production environment without 7 | // further scrutiny and consideration. 8 | 9 | use std::str::FromStr; 10 | 11 | use der::asn1::BitString; 12 | use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; 13 | use polyproto::certs::PublicKeyInfo; 14 | use polyproto::key::{PrivateKey, PublicKey}; 15 | use polyproto::signature::Signature; 16 | use rand::rngs::OsRng; 17 | use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; 18 | 19 | fn main() { 20 | let mut csprng = rand::rngs::OsRng; 21 | // Generate a key pair 22 | let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 23 | println!("Private Key is: {:?}", priv_key.key.to_bytes()); 24 | println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); 25 | println!( 26 | "Public Key OID is: {:?}", 27 | priv_key.public_key.algorithm_identifier() 28 | ); 29 | println!(); 30 | 31 | // Create and sign a message 32 | let message_unsigned = "hi my name is flori".as_bytes(); 33 | let signature = priv_key.sign(message_unsigned); 34 | println!( 35 | "Signature of the message \"{}\": {:?}", 36 | String::from_utf8_lossy(message_unsigned), 37 | signature.as_signature().to_bytes() 38 | ); 39 | 40 | // Verify the signature 41 | println!( 42 | "Is the signature valid? {}", 43 | priv_key 44 | .public_key 45 | .verify_signature(&signature, message_unsigned) 46 | .is_ok() 47 | ); 48 | 49 | // Try to verify the same signature with different data, which should fail 50 | println!( 51 | "Trying again with different data. The result is: {}", 52 | priv_key 53 | .pubkey() 54 | .verify_signature( 55 | &signature, 56 | format!("{} ", String::from_utf8_lossy(message_unsigned)).as_bytes() 57 | ) 58 | .is_ok() 59 | ) 60 | } 61 | 62 | // As mentioned in the README, we start by implementing the signature trait. 63 | 64 | // Here, we start by defining the signature type, which is a wrapper around the signature type from 65 | // the ed25519-dalek crate. 66 | #[derive(Debug, PartialEq, Eq, Clone)] 67 | struct Ed25519Signature { 68 | signature: Ed25519DalekSignature, 69 | algorithm: AlgorithmIdentifierOwned, 70 | } 71 | 72 | impl std::fmt::Display for Ed25519Signature { 73 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 74 | write!(f, "{:?}", self.signature) 75 | } 76 | } 77 | 78 | // We implement the Signature trait for our signature type. 79 | impl Signature for Ed25519Signature { 80 | // We define the signature type from the ed25519-dalek crate as the associated type. 81 | type Signature = Ed25519DalekSignature; 82 | 83 | fn as_bytes(&self) -> Vec { 84 | self.as_signature().to_vec() 85 | } 86 | 87 | // This is straightforward: we return a reference to the signature. 88 | fn as_signature(&self) -> &Self::Signature { 89 | &self.signature 90 | } 91 | 92 | // The algorithm identifier for a given signature implementation is constant. We just need 93 | // to define it here. 94 | fn algorithm_identifier() -> AlgorithmIdentifierOwned { 95 | AlgorithmIdentifierOwned { 96 | // This is the OID for Ed25519. It is defined in the IANA registry. 97 | oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), 98 | // For this example, we don't need or want any parameters. 99 | parameters: None, 100 | } 101 | } 102 | 103 | #[cfg(not(tarpaulin_include))] 104 | fn from_bytes(signature: &[u8]) -> Self { 105 | let mut signature_vec = signature.to_vec(); 106 | signature_vec.resize(64, 0); 107 | let signature_array: [u8; 64] = { 108 | let mut array = [0; 64]; 109 | array.copy_from_slice(&signature_vec[..]); 110 | array 111 | }; 112 | Self { 113 | signature: Ed25519DalekSignature::from_bytes(&signature_array), 114 | algorithm: Self::algorithm_identifier(), 115 | } 116 | } 117 | } 118 | 119 | // The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement 120 | // it for our signature type. 121 | impl SignatureBitStringEncoding for Ed25519Signature { 122 | #[cfg(not(tarpaulin_include))] 123 | fn to_bitstring(&self) -> der::Result { 124 | BitString::from_bytes(&self.as_signature().to_bytes()) 125 | } 126 | } 127 | 128 | // Next, we implement the key traits. We start by defining the private key type. 129 | #[derive(Debug, Clone, PartialEq, Eq)] 130 | struct Ed25519PrivateKey { 131 | // Defined below 132 | public_key: Ed25519PublicKey, 133 | // The private key from the ed25519-dalek crate 134 | key: SigningKey, 135 | } 136 | 137 | impl PrivateKey for Ed25519PrivateKey { 138 | type PublicKey = Ed25519PublicKey; 139 | 140 | // Return a reference to the public key 141 | fn pubkey(&self) -> &Self::PublicKey { 142 | &self.public_key 143 | } 144 | 145 | // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can 146 | // harness all of its functionality, such as the `sign` method. 147 | fn sign(&self, data: &[u8]) -> Ed25519Signature { 148 | let signature = self.key.sign(data); 149 | Ed25519Signature { 150 | signature, 151 | algorithm: self.algorithm_identifier(), 152 | } 153 | } 154 | } 155 | 156 | impl Ed25519PrivateKey { 157 | // Let's also define a handy method to generate a key pair. 158 | pub fn gen_keypair(csprng: &mut OsRng) -> Self { 159 | let key = SigningKey::generate(csprng); 160 | let public_key = Ed25519PublicKey { 161 | key: key.verifying_key(), 162 | }; 163 | Self { public_key, key } 164 | } 165 | } 166 | 167 | // Same thing as above for the public key type. 168 | #[derive(Debug, Clone, PartialEq, Eq)] 169 | struct Ed25519PublicKey { 170 | // The public key type from the ed25519-dalek crate 171 | key: VerifyingKey, 172 | } 173 | 174 | impl PublicKey for Ed25519PublicKey { 175 | // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. 176 | // This method is used to mitigate weak key forgery. 177 | fn verify_signature( 178 | &self, 179 | signature: &Ed25519Signature, 180 | data: &[u8], 181 | ) -> Result<(), polyproto::errors::composite::PublicKeyError> { 182 | match self.key.verify_strict(data, signature.as_signature()) { 183 | Ok(_) => Ok(()), 184 | Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), 185 | } 186 | } 187 | 188 | // Returns the public key info. Public key info is used to encode the public key in a 189 | // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 190 | // standard, and thus includes the information needed to encode the public key in a certificate 191 | // or a CSR. 192 | #[cfg(not(tarpaulin_include))] 193 | fn public_key_info(&self) -> PublicKeyInfo { 194 | PublicKeyInfo { 195 | algorithm: Ed25519Signature::algorithm_identifier(), 196 | public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), 197 | } 198 | } 199 | 200 | #[cfg(not(tarpaulin_include))] 201 | fn try_from_public_key_info( 202 | public_key_info: PublicKeyInfo, 203 | ) -> Result { 204 | let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); 205 | key_vec.resize(32, 0); 206 | let signature_array: [u8; 32] = { 207 | let mut array = [0; 32]; 208 | array.copy_from_slice(&key_vec[..]); 209 | array 210 | }; 211 | Ok(Self { 212 | key: VerifyingKey::from_bytes(&signature_array).unwrap(), 213 | }) 214 | } 215 | } 216 | 217 | #[test] 218 | fn test_example() { 219 | main() 220 | } 221 | -------------------------------------------------------------------------------- /examples/ed25519_cert.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::str::FromStr; 6 | use std::time::Duration; 7 | 8 | use der::Encode; 9 | use der::asn1::{BitString, Uint, UtcTime}; 10 | use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; 11 | use polyproto::certs::capabilities::Capabilities; 12 | use polyproto::certs::idcert::IdCert; 13 | use polyproto::certs::{PublicKeyInfo, Target}; 14 | use polyproto::key::{PrivateKey, PublicKey}; 15 | use polyproto::signature::Signature; 16 | use rand::rngs::OsRng; 17 | use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; 18 | use x509_cert::Certificate; 19 | use x509_cert::name::RdnSequence; 20 | use x509_cert::time::{Time, Validity}; 21 | 22 | /// The following example uses the same setup as in ed25519_basic.rs, but in its main method, it 23 | /// creates a certificate signing request (CSR) and writes it to a file. The CSR is created from a 24 | /// polyproto ID CSR, which is a wrapper around a PKCS #10 CSR. 25 | /// 26 | /// If you have openssl installed, you can inspect the CSR by running: 27 | /// 28 | /// ```sh 29 | /// openssl req -in cert.csr -verify -inform der 30 | /// ``` 31 | /// 32 | /// After that, the program creates an ID-Cert from the given ID-CSR. The `cert.der` file can also 33 | /// be validated using openssl: 34 | /// 35 | /// ```sh 36 | /// openssl x509 -in cert.der -text -noout -inform der 37 | /// ``` 38 | fn main() { 39 | let mut csprng = rand::rngs::OsRng; 40 | let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 41 | println!("Private Key is: {:?}", priv_key.key.to_bytes()); 42 | println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); 43 | println!(); 44 | 45 | let csr = polyproto::certs::idcsr::IdCsr::new( 46 | &RdnSequence::from_str( 47 | "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", 48 | ) 49 | .unwrap(), 50 | &priv_key, 51 | &Capabilities::default_actor(), 52 | Some(Target::Actor), 53 | ) 54 | .unwrap(); 55 | let data = csr.clone().to_der().unwrap(); 56 | let file_name_with_extension = "cert.csr"; 57 | std::fs::write(file_name_with_extension, data).unwrap(); 58 | 59 | let cert = IdCert::from_actor_csr( 60 | csr, 61 | &priv_key, 62 | Uint::new(&8932489u64.to_be_bytes()).unwrap(), 63 | RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), 64 | Validity { 65 | not_before: Time::UtcTime( 66 | UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), 67 | ), 68 | not_after: Time::UtcTime( 69 | UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), 70 | ), 71 | }, 72 | ) 73 | .unwrap(); 74 | let data = Certificate::try_from(cert).unwrap().to_der().unwrap(); 75 | let file_name_with_extension = "cert.der"; 76 | #[cfg(not(target_arch = "wasm32"))] 77 | std::fs::write(file_name_with_extension, data).unwrap(); 78 | } 79 | 80 | // As mentioned in the README, we start by implementing the signature trait. 81 | 82 | // Here, we start by defining the signature type, which is a wrapper around the signature type from 83 | // the ed25519-dalek crate. 84 | #[derive(Debug, PartialEq, Eq, Clone)] 85 | struct Ed25519Signature { 86 | signature: Ed25519DalekSignature, 87 | algorithm: AlgorithmIdentifierOwned, 88 | } 89 | 90 | impl std::fmt::Display for Ed25519Signature { 91 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 92 | write!(f, "{:?}", self.signature) 93 | } 94 | } 95 | 96 | // We implement the Signature trait for our signature type. 97 | impl Signature for Ed25519Signature { 98 | // We define the signature type from the ed25519-dalek crate as the associated type. 99 | type Signature = Ed25519DalekSignature; 100 | 101 | fn as_bytes(&self) -> Vec { 102 | self.as_signature().to_vec() 103 | } 104 | 105 | // This is straightforward: we return a reference to the signature. 106 | fn as_signature(&self) -> &Self::Signature { 107 | &self.signature 108 | } 109 | 110 | // The algorithm identifier for a given signature implementation is constant. We just need 111 | // to define it here. 112 | fn algorithm_identifier() -> AlgorithmIdentifierOwned { 113 | AlgorithmIdentifierOwned { 114 | // This is the OID for Ed25519. It is defined in the IANA registry. 115 | oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), 116 | // For this example, we don't need or want any parameters. 117 | parameters: None, 118 | } 119 | } 120 | 121 | #[cfg(not(tarpaulin_include))] 122 | fn from_bytes(signature: &[u8]) -> Self { 123 | let mut signature_vec = signature.to_vec(); 124 | signature_vec.resize(64, 0); 125 | let signature_array: [u8; 64] = { 126 | let mut array = [0; 64]; 127 | array.copy_from_slice(&signature_vec[..]); 128 | array 129 | }; 130 | Self { 131 | signature: Ed25519DalekSignature::from_bytes(&signature_array), 132 | algorithm: Self::algorithm_identifier(), 133 | } 134 | } 135 | } 136 | 137 | // The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement 138 | // it for our signature type. 139 | impl SignatureBitStringEncoding for Ed25519Signature { 140 | fn to_bitstring(&self) -> der::Result { 141 | BitString::from_bytes(&self.as_signature().to_bytes()) 142 | } 143 | } 144 | 145 | // Next, we implement the key traits. We start by defining the private key type. 146 | #[derive(Debug, Clone, PartialEq, Eq)] 147 | struct Ed25519PrivateKey { 148 | // Defined below 149 | public_key: Ed25519PublicKey, 150 | // The private key from the ed25519-dalek crate 151 | key: SigningKey, 152 | } 153 | 154 | impl PrivateKey for Ed25519PrivateKey { 155 | type PublicKey = Ed25519PublicKey; 156 | 157 | // Return a reference to the public key 158 | fn pubkey(&self) -> &Self::PublicKey { 159 | &self.public_key 160 | } 161 | 162 | // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can 163 | // harness all of its functionality, such as the `sign` method. 164 | fn sign(&self, data: &[u8]) -> Ed25519Signature { 165 | let signature = self.key.sign(data); 166 | Ed25519Signature { 167 | signature, 168 | algorithm: self.algorithm_identifier(), 169 | } 170 | } 171 | } 172 | 173 | impl Ed25519PrivateKey { 174 | // Let's also define a handy method to generate a key pair. 175 | pub fn gen_keypair(csprng: &mut OsRng) -> Self { 176 | let key = SigningKey::generate(csprng); 177 | let public_key = Ed25519PublicKey { 178 | key: key.verifying_key(), 179 | }; 180 | Self { public_key, key } 181 | } 182 | } 183 | 184 | // Same thing as above for the public key type. 185 | #[derive(Debug, Clone, PartialEq, Eq)] 186 | struct Ed25519PublicKey { 187 | // The public key type from the ed25519-dalek crate 188 | key: VerifyingKey, 189 | } 190 | 191 | impl PublicKey for Ed25519PublicKey { 192 | // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. 193 | // This method is used to mitigate weak key forgery. 194 | #[cfg(not(tarpaulin_include))] 195 | fn verify_signature( 196 | &self, 197 | signature: &Ed25519Signature, 198 | data: &[u8], 199 | ) -> Result<(), polyproto::errors::composite::PublicKeyError> { 200 | match self.key.verify_strict(data, signature.as_signature()) { 201 | Ok(_) => Ok(()), 202 | Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), 203 | } 204 | } 205 | 206 | // Returns the public key info. Public key info is used to encode the public key in a 207 | // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 208 | // standard, and thus includes the information needed to encode the public key in a certificate 209 | // or a CSR. 210 | fn public_key_info(&self) -> PublicKeyInfo { 211 | PublicKeyInfo { 212 | algorithm: Ed25519Signature::algorithm_identifier(), 213 | public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), 214 | } 215 | } 216 | 217 | #[cfg(not(tarpaulin_include))] 218 | fn try_from_public_key_info( 219 | public_key_info: PublicKeyInfo, 220 | ) -> std::result::Result< 221 | Ed25519PublicKey, 222 | polyproto::errors::composite::CertificateConversionError, 223 | > { 224 | let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); 225 | key_vec.resize(32, 0); 226 | let signature_array: [u8; 32] = { 227 | let mut array = [0; 32]; 228 | array.copy_from_slice(&key_vec[..]); 229 | array 230 | }; 231 | Ok(Self { 232 | key: VerifyingKey::from_bytes(&signature_array).unwrap(), 233 | }) 234 | } 235 | } 236 | 237 | #[test] 238 | fn test_example() { 239 | main() 240 | } 241 | -------------------------------------------------------------------------------- /examples/ed25519_from_der.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::str::FromStr; 6 | use std::time::Duration; 7 | 8 | use der::asn1::{BitString, Uint, UtcTime}; 9 | use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; 10 | use polyproto::certs::capabilities::Capabilities; 11 | use polyproto::certs::idcert::IdCert; 12 | use polyproto::certs::{PublicKeyInfo, Target}; 13 | use polyproto::key::{PrivateKey, PublicKey}; 14 | use polyproto::signature::Signature; 15 | use rand::rngs::OsRng; 16 | use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; 17 | use x509_cert::name::RdnSequence; 18 | use x509_cert::time::{Time, Validity}; 19 | 20 | /// The following example uses the same setup as in ed25519_basic.rs, but in its main method, it 21 | /// creates a certificate signing request (CSR) and writes it to a file. The CSR is created from a 22 | /// polyproto ID CSR, which is a wrapper around a PKCS #10 CSR. 23 | /// 24 | /// If you have openssl installed, you can inspect the CSR by running: 25 | /// 26 | /// ```sh 27 | /// openssl req -in cert.csr -verify -inform der 28 | /// ``` 29 | fn main() { 30 | let mut csprng = rand::rngs::OsRng; 31 | let priv_key_actor = Ed25519PrivateKey::gen_keypair(&mut csprng); 32 | let priv_key_home_server = Ed25519PrivateKey::gen_keypair(&mut csprng); 33 | println!("Private Key is: {:?}", priv_key_actor.key.to_bytes()); 34 | println!( 35 | "Public Key is: {:?}", 36 | priv_key_actor.public_key.key.to_bytes() 37 | ); 38 | println!(); 39 | 40 | let csr = polyproto::certs::idcsr::IdCsr::new( 41 | &RdnSequence::from_str( 42 | "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", 43 | ) 44 | .unwrap(), 45 | &priv_key_actor, 46 | &Capabilities::default_actor(), 47 | Some(polyproto::certs::Target::Actor), 48 | ) 49 | .unwrap(); 50 | 51 | let data = csr.clone().to_der().unwrap(); 52 | let file_name_with_extension = "cert.csr"; 53 | #[cfg(not(target_arch = "wasm32"))] 54 | std::fs::write(file_name_with_extension, data).unwrap(); 55 | 56 | let cert = IdCert::from_actor_csr( 57 | csr, 58 | &priv_key_home_server, 59 | Uint::new(&8932489u64.to_be_bytes()).unwrap(), 60 | RdnSequence::from_str("DC=polyphony,DC=chat").unwrap(), 61 | Validity { 62 | not_before: Time::UtcTime( 63 | UtcTime::from_unix_duration(Duration::from_secs(10)).unwrap(), 64 | ), 65 | not_after: Time::UtcTime( 66 | UtcTime::from_unix_duration(Duration::from_secs(1000)).unwrap(), 67 | ), 68 | }, 69 | ) 70 | .unwrap(); 71 | let data = cert.clone().to_der().unwrap(); 72 | // ``::from_der()` performs a full check of the certificate, including signature verification. 73 | let cert_from_der = 74 | IdCert::from_der(&data, Target::Actor, 15, &priv_key_home_server.public_key).unwrap(); 75 | assert_eq!(cert_from_der, cert); 76 | // ...so technically, we don't need to verify the signature again. This is just for demonstration 77 | // of how you would manually verify a certificate. 78 | assert!( 79 | cert_from_der 80 | .full_verify_actor(15, &priv_key_home_server.public_key) 81 | .is_ok() 82 | ) 83 | } 84 | 85 | // As mentioned in the README, we start by implementing the signature trait. 86 | 87 | // Here, we start by defining the signature type, which is a wrapper around the signature type from 88 | // the ed25519-dalek crate. 89 | #[derive(Debug, PartialEq, Eq, Clone)] 90 | struct Ed25519Signature { 91 | signature: Ed25519DalekSignature, 92 | algorithm: AlgorithmIdentifierOwned, 93 | } 94 | 95 | impl std::fmt::Display for Ed25519Signature { 96 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 97 | write!(f, "{:?}", self.signature) 98 | } 99 | } 100 | 101 | // We implement the Signature trait for our signature type. 102 | impl Signature for Ed25519Signature { 103 | // We define the signature type from the ed25519-dalek crate as the associated type. 104 | type Signature = Ed25519DalekSignature; 105 | 106 | fn as_bytes(&self) -> Vec { 107 | self.as_signature().to_vec() 108 | } 109 | 110 | // This is straightforward: we return a reference to the signature. 111 | fn as_signature(&self) -> &Self::Signature { 112 | &self.signature 113 | } 114 | 115 | // The algorithm identifier for a given signature implementation is constant. We just need 116 | // to define it here. 117 | fn algorithm_identifier() -> AlgorithmIdentifierOwned { 118 | AlgorithmIdentifierOwned { 119 | // This is the OID for Ed25519. It is defined in the IANA registry. 120 | oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), 121 | // For this example, we don't need or want any parameters. 122 | parameters: None, 123 | } 124 | } 125 | 126 | fn from_bytes(signature: &[u8]) -> Self { 127 | let mut signature_vec = signature.to_vec(); 128 | signature_vec.resize(64, 0); 129 | let signature_array: [u8; 64] = { 130 | let mut array = [0; 64]; 131 | array.copy_from_slice(&signature_vec[..]); 132 | array 133 | }; 134 | Self { 135 | signature: Ed25519DalekSignature::from_bytes(&signature_array), 136 | algorithm: Self::algorithm_identifier(), 137 | } 138 | } 139 | } 140 | 141 | // The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement 142 | // it for our signature type. 143 | impl SignatureBitStringEncoding for Ed25519Signature { 144 | fn to_bitstring(&self) -> der::Result { 145 | BitString::from_bytes(&self.as_signature().to_bytes()) 146 | } 147 | } 148 | 149 | // Next, we implement the key traits. We start by defining the private key type. 150 | #[derive(Debug, Clone, PartialEq, Eq)] 151 | struct Ed25519PrivateKey { 152 | // Defined below 153 | public_key: Ed25519PublicKey, 154 | // The private key from the ed25519-dalek crate 155 | key: SigningKey, 156 | } 157 | 158 | impl PrivateKey for Ed25519PrivateKey { 159 | type PublicKey = Ed25519PublicKey; 160 | 161 | // Return a reference to the public key 162 | fn pubkey(&self) -> &Self::PublicKey { 163 | &self.public_key 164 | } 165 | 166 | // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can 167 | // harness all of its functionality, such as the `sign` method. 168 | fn sign(&self, data: &[u8]) -> Ed25519Signature { 169 | let signature = self.key.sign(data); 170 | Ed25519Signature { 171 | signature, 172 | algorithm: self.algorithm_identifier(), 173 | } 174 | } 175 | } 176 | 177 | impl Ed25519PrivateKey { 178 | // Let's also define a handy method to generate a key pair. 179 | pub fn gen_keypair(csprng: &mut OsRng) -> Self { 180 | let key = SigningKey::generate(csprng); 181 | let public_key = Ed25519PublicKey { 182 | key: key.verifying_key(), 183 | }; 184 | Self { public_key, key } 185 | } 186 | } 187 | 188 | // Same thing as above for the public key type. 189 | #[derive(Debug, Clone, PartialEq, Eq)] 190 | struct Ed25519PublicKey { 191 | // The public key type from the ed25519-dalek crate 192 | key: VerifyingKey, 193 | } 194 | 195 | impl PublicKey for Ed25519PublicKey { 196 | // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. 197 | // This method is used to mitigate weak key forgery. 198 | fn verify_signature( 199 | &self, 200 | signature: &Ed25519Signature, 201 | data: &[u8], 202 | ) -> Result<(), polyproto::errors::composite::PublicKeyError> { 203 | match self.key.verify_strict(data, signature.as_signature()) { 204 | Ok(_) => Ok(()), 205 | Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), 206 | } 207 | } 208 | 209 | // Returns the public key info. Public key info is used to encode the public key in a 210 | // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 211 | // standard, and thus includes the information needed to encode the public key in a certificate 212 | // or a CSR. 213 | fn public_key_info(&self) -> PublicKeyInfo { 214 | PublicKeyInfo { 215 | algorithm: Ed25519Signature::algorithm_identifier(), 216 | public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), 217 | } 218 | } 219 | 220 | fn try_from_public_key_info( 221 | public_key_info: PublicKeyInfo, 222 | ) -> std::result::Result< 223 | Ed25519PublicKey, 224 | polyproto::errors::composite::CertificateConversionError, 225 | > { 226 | let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); 227 | key_vec.resize(32, 0); 228 | let signature_array: [u8; 32] = { 229 | let mut array = [0; 32]; 230 | array.copy_from_slice(&key_vec[..]); 231 | array 232 | }; 233 | Ok(Self { 234 | key: VerifyingKey::from_bytes(&signature_array).unwrap(), 235 | }) 236 | } 237 | } 238 | 239 | #[test] 240 | fn test_example() { 241 | main() 242 | } 243 | -------------------------------------------------------------------------------- /examples/http_api.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::str::FromStr; 6 | 7 | use der::asn1::BitString; 8 | use ed25519_dalek::ed25519::signature::Signer; 9 | use ed25519_dalek::{Signature as Ed25519DalekSignature, SigningKey, VerifyingKey}; 10 | use httptest::matchers::request; 11 | use httptest::responders::json_encoded; 12 | use httptest::{Expectation, Server}; 13 | use polyproto::api::HttpClient; 14 | use polyproto::api::core::current_unix_time; 15 | use polyproto::certs::PublicKeyInfo; 16 | use polyproto::errors::CertificateConversionError; 17 | use polyproto::key::{PrivateKey, PublicKey}; 18 | use polyproto::signature::Signature; 19 | use polyproto::types::routes::core::v1::GET_CHALLENGE_STRING; 20 | use serde_json::json; 21 | use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; 22 | use url::Url; 23 | 24 | async fn setup_example() -> Server { 25 | let server = Server::run(); 26 | server.expect( 27 | Expectation::matching(request::method_path( 28 | GET_CHALLENGE_STRING.method.as_str(), 29 | GET_CHALLENGE_STRING.path, 30 | )) 31 | .respond_with(json_encoded(json!({ 32 | "challenge": "abcd".repeat(8), 33 | "expires": current_unix_time() + 100 34 | }))), 35 | ); 36 | server 37 | } 38 | 39 | #[cfg(not(test))] 40 | #[tokio::main] 41 | async fn main() { 42 | let server = setup_example().await; 43 | let url = format!("http://{}", server.addr()); 44 | 45 | // The actual example starts here. 46 | // Create a new HTTP client 47 | let client = HttpClient::new().unwrap(); 48 | // Create an authorized session, if you need it 49 | let _session: polyproto::api::Session = 50 | polyproto::api::Session::new(&client, "12345", Url::parse(&url).unwrap(), None); 51 | // You can now use the client and session to make requests to the polyproto home server! 52 | // The client is responsible for all unauthenticated requests, while sessions handle all 53 | // the routes needing authentication of some sort. 54 | // Routes are documented under , and each route has a 55 | // corresponding method in the `HttpClient` struct. For example, if we wanted to get the certificate 56 | // of the home server, we'd call: 57 | let cert = client 58 | .get_server_id_cert(None, &Url::parse("https://example.com/").unwrap()) 59 | .await 60 | .unwrap(); 61 | dbg!(cert); 62 | } 63 | 64 | #[cfg(test)] 65 | fn main() { 66 | main() 67 | } 68 | 69 | #[derive(Debug, PartialEq, Eq, Clone)] 70 | pub(crate) struct Ed25519Signature { 71 | pub(crate) signature: Ed25519DalekSignature, 72 | pub(crate) algorithm: AlgorithmIdentifierOwned, 73 | } 74 | 75 | impl std::fmt::Display for Ed25519Signature { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | write!(f, "{:?}", self.signature) 78 | } 79 | } 80 | 81 | // We implement the Signature trait for our signature type. 82 | impl Signature for Ed25519Signature { 83 | // We define the signature type from the ed25519-dalek crate as the associated type. 84 | type Signature = Ed25519DalekSignature; 85 | 86 | // This is straightforward: we return a reference to the signature. 87 | fn as_signature(&self) -> &Self::Signature { 88 | &self.signature 89 | } 90 | 91 | // The algorithm identifier for a given signature implementation is constant. We just need 92 | // to define it here. 93 | fn algorithm_identifier() -> AlgorithmIdentifierOwned { 94 | AlgorithmIdentifierOwned { 95 | // This is the OID for Ed25519. It is defined in the IANA registry. 96 | oid: ObjectIdentifier::from_str("1.3.101.112").unwrap(), 97 | // For this example, we don't need or want any parameters. 98 | parameters: None, 99 | } 100 | } 101 | 102 | fn from_bytes(signature: &[u8]) -> Self { 103 | let mut signature_vec = signature.to_vec(); 104 | signature_vec.resize(64, 0); 105 | let signature_array: [u8; 64] = { 106 | let mut array = [0; 64]; 107 | array.copy_from_slice(&signature_vec[..]); 108 | array 109 | }; 110 | Self { 111 | signature: Ed25519DalekSignature::from_bytes(&signature_array), 112 | algorithm: Self::algorithm_identifier(), 113 | } 114 | } 115 | 116 | fn as_bytes(&self) -> Vec { 117 | self.as_signature().to_vec() 118 | } 119 | } 120 | 121 | // The `SignatureBitStringEncoding` trait is used to convert a signature to a bit string. We implement 122 | // it for our signature type. 123 | impl SignatureBitStringEncoding for Ed25519Signature { 124 | fn to_bitstring(&self) -> der::Result { 125 | BitString::from_bytes(&self.as_signature().to_bytes()) 126 | } 127 | } 128 | 129 | // Next, we implement the key traits. We start by defining the private key type. 130 | #[derive(Debug, Clone, PartialEq, Eq)] 131 | pub(crate) struct Ed25519PrivateKey { 132 | // Defined below 133 | pub(crate) public_key: Ed25519PublicKey, 134 | // The private key from the ed25519-dalek crate 135 | pub(crate) key: SigningKey, 136 | } 137 | 138 | impl PrivateKey for Ed25519PrivateKey { 139 | type PublicKey = Ed25519PublicKey; 140 | 141 | // Return a reference to the public key 142 | fn pubkey(&self) -> &Self::PublicKey { 143 | &self.public_key 144 | } 145 | 146 | // Signs a message. The beauty of having to wrap the ed25519-dalek crate is that we can 147 | // harness all of its functionality, such as the `sign` method. 148 | fn sign(&self, data: &[u8]) -> Ed25519Signature { 149 | let signature = self.key.sign(data); 150 | Ed25519Signature { 151 | signature, 152 | algorithm: self.algorithm_identifier(), 153 | } 154 | } 155 | } 156 | 157 | // Same thing as above for the public key type. 158 | #[derive(Debug, Clone, PartialEq, Eq)] 159 | pub(crate) struct Ed25519PublicKey { 160 | // The public key type from the ed25519-dalek crate 161 | pub(crate) key: VerifyingKey, 162 | } 163 | 164 | impl PublicKey for Ed25519PublicKey { 165 | // Verifies a signature. We use the `verify_strict` method from the ed25519-dalek crate. 166 | // This method is used to mitigate weak key forgery. 167 | fn verify_signature( 168 | &self, 169 | signature: &Ed25519Signature, 170 | data: &[u8], 171 | ) -> Result<(), polyproto::errors::composite::PublicKeyError> { 172 | match self.key.verify_strict(data, signature.as_signature()) { 173 | Ok(_) => Ok(()), 174 | Err(_) => Err(polyproto::errors::composite::PublicKeyError::BadSignature), 175 | } 176 | } 177 | 178 | // Returns the public key info. Public key info is used to encode the public key in a 179 | // certificate or a CSR. It is named after the `SubjectPublicKeyInfo` type from the X.509 180 | // standard, and thus includes the information needed to encode the public key in a certificate 181 | // or a CSR. 182 | fn public_key_info(&self) -> PublicKeyInfo { 183 | PublicKeyInfo { 184 | algorithm: Ed25519Signature::algorithm_identifier(), 185 | public_key_bitstring: BitString::from_bytes(&self.key.to_bytes()).unwrap(), 186 | } 187 | } 188 | 189 | fn try_from_public_key_info( 190 | public_key_info: PublicKeyInfo, 191 | ) -> Result { 192 | let mut key_vec = public_key_info.public_key_bitstring.raw_bytes().to_vec(); 193 | key_vec.resize(32, 0); 194 | let signature_array: [u8; 32] = { 195 | let mut array = [0; 32]; 196 | array.copy_from_slice(&key_vec[..]); 197 | array 198 | }; 199 | Ok(Self { 200 | key: VerifyingKey::from_bytes(&signature_array).unwrap(), 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /gen-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | # If you do not tarpaulin installed, you need to do so before running this command. 8 | 9 | cargo tarpaulin --all-features --tests --verbose -o lcov --output-dir .coverage --fail-immediately --avoid-cfg-tarpaulin -------------------------------------------------------------------------------- /headaches.md: -------------------------------------------------------------------------------- 1 | # Headaches involving `wasm-bindgen` and overall Rust ⇾ TS/JS bindgen 2 | 3 | - `wasm-bindgen` does not support traits, or struct generics with trait bounds like `struct X` 4 | - Crates like `ts_rs` might help but would require a more complex build process to assemble a finished TS/JS project 5 | - Worst case: Manually write JS/TS code to bridge the things unsupported by other bindgen libs 6 | - Would be immensely painful 7 | - Other idea: LOADS of handwritten wrappers for Rust functions, also written in Rust but `wasm-bindgen` compatible. 8 | - For traits like signature, we need to make an extremely generic impl that can be somehow instantiated from js/ts -------------------------------------------------------------------------------- /scripts/check-license-header.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | # Execute this from the project root: 6 | # $: ./scripts/check-license-header.sh 7 | npx github:viperproject/check-license-header#v2 check -c .github/license-check/config.json --path . -------------------------------------------------------------------------------- /scripts/fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | # Execute this from the project root: 6 | # $: ./scripts/fmt.sh 7 | cargo fmt && cargo clippy --fix --allow-dirty --allow-staged -------------------------------------------------------------------------------- /scripts/gen-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | # If you do not tarpaulin installed, you need to do so before running this command. 8 | 9 | cargo tarpaulin --avoid-cfg-tarpaulin --features="types,reqwest,gateway" --tests --verbose -o lcov --output-dir .coverage -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | cargo nextest run --features="gateway, types, reqwest" --failure-output final --no-fail-fast 6 | -------------------------------------------------------------------------------- /scripts/wasm-pack-helper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | 6 | # check number of arguments 7 | if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then 8 | echo "Error: Invalid number of arguments. Usage: $0 [release|debug]" 9 | exit 1 10 | fi 11 | 12 | command=$1 13 | mode=${2:-"debug"} 14 | 15 | # validate if arg #1 is valid 16 | if [[ "$command" != "build" && "$command" != "pack" && "$command" != "publish" ]]; then 17 | echo "Error: First argument must be one of 'build', 'pack', or 'publish'." 18 | exit 1 19 | fi 20 | 21 | # same for arg #2 22 | if [[ "$mode" != "release" && "$mode" != "debug" ]]; then 23 | echo "Error: Second argument must be either 'release' or 'debug'. Defaulting to 'debug'." 24 | mode="debug" 25 | fi 26 | 27 | # wasm-pack check 28 | if ! command -v wasm-pack &> /dev/null; then 29 | # prompt user to install wasm-pack 30 | read -p "wasm-pack could not be found. Do you want to install it? (y/n): " install_wasm_pack 31 | if [[ "$install_wasm_pack" == [Yy]* ]]; then 32 | cargo install wasm-pack --force 33 | else 34 | echo "Error: wasm-pack is required for this script." 35 | exit 1 36 | fi 37 | fi 38 | 39 | # Execute the wasm-pack command 40 | wasm-pack $command --$mode --no-default-features --features=wasm,reqwest,serde,types,_wasm_bindgen -------------------------------------------------------------------------------- /src/api/cacheable_cert.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use der::{Any, DecodePem}; 6 | use log::{debug, trace}; 7 | use spki::AlgorithmIdentifier; 8 | 9 | use crate::certs::Target; 10 | use crate::certs::idcert::IdCert; 11 | use crate::key::PublicKey; 12 | use crate::signature::Signature; 13 | use crate::types::der::asn1::Uint; 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 16 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 17 | #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] 18 | /// A cacheable response to an [IdCert] request. 19 | pub struct CacheableIdCert { 20 | #[cfg_attr(feature = "serde", serde(rename = "idCertPem"))] 21 | /// The requested ID-Cert in ASCII PEM format. 22 | pub cert: String, 23 | #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] 24 | /// UNIX timestamp that specifies when this specific id_cert has been marked as invalidated by the server. 25 | /// An ID-Cert is considered invalidated, if the server or actor choose to revoke the validity of the 26 | /// ID-Cert before the lifetime of the certificate was scheduled to end. If this property does not exist, 27 | /// the ID-Cert has not been invalidated. 28 | pub invalidated_at: Option, 29 | #[cfg_attr(feature = "serde", serde(rename = "cacheNotValidBefore"))] 30 | /// UNIX timestamp that specifies the time from which this cache entry may be treated as valid. 31 | pub not_valid_before: u64, 32 | #[cfg_attr(feature = "serde", serde(rename = "cacheNotValidAfter"))] 33 | /// UNIX timestamp that specifies a time until which this cache entry may be treated as valid. 34 | pub not_valid_after: u64, 35 | /// Signature generated by the home server. This signature can be verified using the home servers' 36 | /// public identity key. A server generates the cacheSignature by concatenating the serial number 37 | /// of the ID-Cert in question with the cacheValidNotBefore timestamp and the cacheValidNotAfter timestamp, 38 | /// then generating the signature of the resulting concatenated string using the private identity 39 | /// key of the server. Clients must reject certificates of which the cacheSignature can not be 40 | /// verified to be correct. 41 | pub cache_signature: String, 42 | } 43 | 44 | impl CacheableIdCert { 45 | /// Convert [Self] to an [IdCert]. Requires knowing [Signature] and [PublicKey] types. Finding 46 | /// out which algorithm is being used can be facilitated using [Self::algorithm_identifier]. 47 | /// 48 | /// ## Parameters 49 | /// 50 | /// - `time`: UNIX Timestamp; Certificate validity will be tested for given this timestamp. 51 | /// 52 | /// ## Errors 53 | /// 54 | /// Will error, if 55 | /// 56 | /// - The wrong [Target] is selected 57 | /// - The certificate is not well-formed 58 | /// - The certificate is invalid (wrong signature/pubkey) 59 | pub fn try_to_idcert>( 60 | &self, 61 | target: Target, 62 | time: u64, 63 | home_server_public_key: &P, 64 | ) -> Result, crate::errors::CertificateConversionError> { 65 | Ok(IdCert::from_pem( 66 | &self.cert, 67 | target, 68 | time, 69 | home_server_public_key, 70 | )?) 71 | } 72 | 73 | /// Retrieve the [AlgorithmIdentifier] of the certificate. This output can then be used to figure 74 | /// out, which concrete variant of [IdCert] to construct from the contents of [Self]. 75 | /// 76 | /// ## Errors 77 | /// 78 | /// This method will error if `self.cert` is an invalid X.509 v3 certificate. 79 | pub fn algorithm_identifier( 80 | &self, 81 | ) -> Result, crate::errors::CertificateConversionError> { 82 | let certificate = x509_cert::Certificate::from_pem(&self.cert)?; 83 | Ok(certificate.signature_algorithm) 84 | } 85 | 86 | // TODO: Test me 87 | /// Verifies the cache integrity of a cached ID-Cert using the provided verifying key. See the 88 | /// "Safety" section below for more information. 89 | /// 90 | /// This function performs the following steps: 91 | /// - Parses the [IdCert] from its PEM representation (unchecked!). 92 | /// - Validates the certificate's 93 | /// [serial number](https://docs.polyphony.chat/Protocol%20Specifications/core/#:~:text=64-bit%20unsigned-,integer.,-Serial%20Number) 94 | /// and checks if it is within the expected range. 95 | /// - Constructs a verification string by concatenating: 96 | /// - The certificate's serial number. 97 | /// - The `not_valid_before` timestamp. 98 | /// - The `not_valid_after` timestamp. 99 | /// - The `invalidated_at` timestamp, if present, or an empty string. 100 | /// - Verifies the `cacheSignature` using the `verifying_key` against the constructed string. 101 | /// 102 | /// # Parameters 103 | /// - `verifying_key`: A reference to the public key used for signature verification. In this case, 104 | /// this key should be the public key of the home server which issued this certificate. 105 | /// 106 | /// # Returns 107 | /// - `Ok(())` The `cacheSignature` could be verified to be correct. 108 | /// - `Err(crate::errors::InvalidCert)` if the verification fails or the certificate contains invalid data. 109 | /// 110 | /// # Errors 111 | /// - Returns an [InvalidProperties] error if the PEM parsing or serial number validation fails. 112 | /// - Returns a [PublicKeyError] if the signature verification fails. 113 | /// 114 | /// # Safety 115 | /// This function: 116 | /// 117 | /// - ensures the `cacheSignature` is correctly generated by the server using the serial number, 118 | /// `not_valid_before`, `not_valid_after`, and optionally `invalidated_at` timestamps in the 119 | /// correct order. 120 | /// - rejects certificates where the cache signature does not match the computed signature. 121 | /// 122 | /// This function **does not:** 123 | /// - ensure that the contents of the certificate are correct 124 | /// - check if the certificate is well-formed, though passing an invalid certificate *might* result 125 | /// in an [InvalidProperties] parsing error. 126 | /// 127 | /// Use the dedicated certificate validation methods to ensure that the certificate is correct. 128 | /// 129 | /// # Example 130 | /// ```rs 131 | /// let cert = IdCert::<...,...> { /* data */ }; 132 | /// let verifying_key = MyPublicKey::new(); 133 | /// match cert.verify(&verifying_key) { 134 | /// Ok(_) => println!("Certificate is valid."), 135 | /// Err(e) => eprintln!("Certificate verification failed: {:?}", e), 136 | /// } 137 | /// ``` 138 | pub fn verify>( 139 | &self, 140 | verifying_key: &P, 141 | ) -> Result<(), crate::errors::InvalidCert> { 142 | let raw_cert = IdCert::::from_pem_unchecked(&self.cert).map_err(|e| { 143 | crate::errors::InvalidCert::InvalidProperties( 144 | crate::errors::ConstraintError::Malformed(Some(e.to_string())), 145 | ) 146 | })?; 147 | let serial_number = 148 | u64::try_from(Uint(raw_cert.id_cert_tbs.serial_number)).map_err(|e| { 149 | crate::errors::InvalidCert::InvalidProperties( 150 | crate::errors::ConstraintError::Malformed(Some(format!( 151 | "Serial number is not valid: {}", 152 | e 153 | ))), 154 | ) 155 | })?; 156 | let string_to_check = serial_number.to_string() 157 | + &self.not_valid_before.to_string() 158 | + &self.not_valid_after.to_string() 159 | + &self 160 | .invalidated_at 161 | .map(|v| v.to_string()) 162 | .unwrap_or("".to_string()); 163 | trace!("Computed string: {}", string_to_check); 164 | verifying_key 165 | .verify_signature( 166 | &S::try_from_hex(&self.cache_signature).map_err(|e| { 167 | debug!("{e}"); 168 | crate::errors::InvalidCert::PublicKeyError( 169 | crate::errors::PublicKeyError::BadSignature, 170 | ) 171 | })?, 172 | string_to_check.as_bytes(), 173 | ) 174 | .map(|_| ()) 175 | .map_err(crate::errors::InvalidCert::PublicKeyError) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/api/core/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::time::UNIX_EPOCH; 6 | 7 | use crate::certs::idcerttbs::IdCertTbs; 8 | use crate::url::Url; 9 | 10 | use crate::key::PublicKey; 11 | use crate::signature::Signature; 12 | 13 | use super::cacheable_cert::CacheableIdCert; 14 | use crate::types::x509_cert::SerialNumber; 15 | use log::trace; 16 | 17 | use crate::certs::SessionId; 18 | use crate::certs::idcert::IdCert; 19 | #[cfg(feature = "reqwest")] 20 | use crate::errors::RequestError; 21 | use crate::key::PrivateKey; 22 | use crate::types::routes::core::v1::*; 23 | use crate::types::{EncryptedPkm, FederationId, Service, ServiceName}; 24 | 25 | #[cfg(feature = "reqwest")] 26 | use super::{HttpClient, HttpResult, Session}; 27 | 28 | #[cfg(feature = "reqwest")] 29 | mod federated_identity; 30 | #[cfg(feature = "reqwest")] 31 | mod migration; 32 | #[cfg(feature = "reqwest")] 33 | mod rawr; 34 | #[cfg(feature = "reqwest")] 35 | mod services; 36 | 37 | #[cfg(feature = "reqwest")] 38 | pub use routes::*; 39 | 40 | /// Get the current UNIX timestamp according to the system clock. 41 | pub fn current_unix_time() -> u64 { 42 | std::time::SystemTime::now() 43 | .duration_since(UNIX_EPOCH) 44 | .unwrap() 45 | .as_secs() 46 | } 47 | 48 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 49 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 50 | /// Response from querying a polyproto `.well-known` endpoint. 51 | pub struct WellKnown { 52 | api: Url, 53 | } 54 | 55 | impl WellKnown { 56 | /// Return the [Url] that this .well-known entry points to. 57 | pub fn api(&self) -> &Url { 58 | &self.api 59 | } 60 | 61 | /// Create [Self] from a [Url], setting the `api` field to the supplied URL without performing any 62 | /// validity checks. Use [Self::new()] if you want to create [Self] from a link to a "visible domain". 63 | pub fn from_url(url: &Url) -> Self { 64 | Self::from(url.clone()) 65 | } 66 | 67 | /** 68 | Checks whether the "visible domain" in a certificate matches the "actual url" specified by the 69 | `.well-known` endpoint of that "visible domain". 70 | 71 | ## .well-known validation criterions 72 | 73 | > *The following is an excerpt from section 3.1 of the polyproto specification.* 74 | 75 | polyproto servers can be hosted under a domain name different from the domain name appearing on ID-Certs managed by that server if all the following conditions are met: 76 | 1. Define the "visible domain name" as the domain name visible on an ID-Cert. 77 | 2. Define the "actual domain name" as the domain name where the polyproto server is actually hosted under. 78 | 3. The visible domain name must have a URI [visible domain name]/.well-known/polyproto-core, accessible via an HTTP GET request. 79 | 4. The resource accessible at this URI must be a JSON object formatted as such: 80 | 81 | ```json 82 | { 83 | "api": "[actual domain name]/.p2/core/" 84 | } 85 | ``` 86 | 87 | 5. The ID-Cert received when querying [actual domain name]/.p2/core/idcert/server with an HTTP 88 | GET request must have a field "issuer" containing domain components (dc) that, when parsed, 89 | equal the domain name of the visible domain name. If the domain components in this field do 90 | not match the domain components of the visible domain name, the server hosted under the actual 91 | domain name must not be treated as a polyproto server for the visible domain name. 92 | 93 | If all the above-mentioned conditions can be fulfilled, the client 94 | can treat the server located at the actual domain name as a polyproto server serving the visible domain 95 | name. Clients must not treat the server located at the actual domain name as a polyproto server 96 | serving the actual domain name. 97 | 98 | ## TL;DR 99 | 100 | This function verifies these 5 criteria. If all of these criteria 101 | are fulfilled, `true` is returned. If any of the criteria are not fulfilled, `false` is returned. 102 | Criterion #3 is fulfilled by the existence of this struct object. 103 | */ 104 | pub fn matches_certificate>( 105 | &self, 106 | cert: &IdCertTbs, 107 | ) -> bool { 108 | let visible_domain = match cert.issuer_url() { 109 | Ok(url) => url, 110 | Err(_) => return false, 111 | }; 112 | let actual_domain = &self.api.host(); 113 | trace!( 114 | "Checking for equality of {:?} and {:?}", 115 | visible_domain.host(), 116 | *actual_domain 117 | ); 118 | visible_domain.host() == *actual_domain 119 | } 120 | 121 | /// Request the contents of the polyproto `.well-known` endpoint from a base url. 122 | /// 123 | /// This is a shorthand for 124 | /// ```rs 125 | /// self.request_as::(http::Method::GET, url, None).await 126 | /// ``` 127 | /// 128 | /// ## Errors 129 | /// 130 | /// This method will error if the server is unreachable or if the resource is malformed. 131 | #[cfg(feature = "reqwest")] 132 | pub async fn new(client: &HttpClient, url: &Url) -> HttpResult { 133 | client.get_well_known(url).await 134 | } 135 | } 136 | 137 | impl std::fmt::Display for WellKnown { 138 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 139 | f.write_str(self.api.as_str()) 140 | } 141 | } 142 | 143 | impl From for WellKnown { 144 | /// Does NOT check whether [Self] is valid. Use [Self::new()] instead. 145 | fn from(value: Url) -> Self { 146 | WellKnown { api: value } 147 | } 148 | } 149 | 150 | impl<'a> From<&'a WellKnown> for &'a str { 151 | fn from(value: &'a WellKnown) -> Self { 152 | value.api.as_str() 153 | } 154 | } 155 | 156 | impl From for Url { 157 | fn from(value: WellKnown) -> Self { 158 | value.api 159 | } 160 | } 161 | 162 | impl From for String { 163 | fn from(value: WellKnown) -> Self { 164 | value.api.to_string() 165 | } 166 | } 167 | 168 | #[cfg(feature = "reqwest")] 169 | /// Module containing an implementation of a reqwest-based HTTP client with polyproto routes 170 | /// implemented on it. 171 | pub mod routes { 172 | use serde::{Deserialize, Serialize}; 173 | use url::Url; 174 | 175 | use crate::api::{HttpClient, HttpResult}; 176 | use crate::types::Service; 177 | 178 | use super::WellKnown; 179 | 180 | // Core Routes: No registration needed 181 | impl HttpClient { 182 | /// Request the contents of the polyproto `.well-known` endpoint from a base url. 183 | /// 184 | /// This is a shorthand for 185 | /// ```rs 186 | /// self.request_as::(http::Method::GET, url, None).await 187 | /// ``` 188 | /// 189 | /// ## Errors 190 | /// 191 | /// This method will error if the server is unreachable or if the resource is malformed. 192 | pub async fn get_well_known(&self, url: &Url) -> HttpResult { 193 | let url = url.join(".well-known/polyproto-core")?; 194 | self.request_as(http::Method::GET, url.as_str(), None).await 195 | } 196 | } 197 | 198 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 199 | /// Represents a pair of an [IdCert] and a token, used in the API as a response when an [IdCsr] has 200 | /// been accepted by the server. 201 | pub struct IdCertToken { 202 | /// The [IdCert] as a PEM encoded string 203 | pub id_cert: String, 204 | /// The token as a string 205 | pub token: String, 206 | } 207 | 208 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 209 | #[derive(Debug, Clone, PartialEq, Eq)] 210 | /// Represents a response to a service discovery deletion request. Contains the deleted service 211 | /// and, if applicable, the new primary service provider for the service. 212 | pub struct ServiceDeleteResponse { 213 | /// The service that was deleted. 214 | pub deleted: Service, 215 | /// The new primary service provider for the service, if applicable. 216 | pub new_primary: Option, 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/api/core/rawr.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use super::*; 6 | mod registration_required { 7 | use std::fmt::Display; 8 | 9 | use http::StatusCode; 10 | use serde_json::json; 11 | 12 | use crate::api::matches_status_code; 13 | use crate::types::{Resource, ResourceAccessProperties, ResourceInformation}; 14 | 15 | use super::*; 16 | 17 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 18 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 19 | /// Whether the list of [Resources] should be sorted in a specific way. Specific to the 20 | /// "List uploaded RawR resources"-route. 21 | pub enum Ordering { 22 | /// Smallest first 23 | SizeAsc, 24 | /// Largest first 25 | SizeDesc, 26 | /// Newest first 27 | NewestFirst, 28 | /// Oldest first 29 | OldestFirst, 30 | } 31 | 32 | impl Display for Ordering { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | match self { 35 | Ordering::SizeAsc => f.write_str("SizeAsc"), 36 | Ordering::SizeDesc => f.write_str("SizeDesc"), 37 | Ordering::NewestFirst => f.write_str("NewestFirst"), 38 | Ordering::OldestFirst => f.write_str("OldestFirst"), 39 | } 40 | } 41 | } 42 | 43 | impl> Session { 44 | /// Query the server for a list of resources you've uploaded. 45 | /// 46 | /// ## Parameters 47 | /// 48 | /// - `limit`: How many results you'd like to retrieve at maximum. Usually defaults to 50. 49 | /// - `sort`: Whether the list should be sorted in a specific way. 50 | pub async fn list_uploaded_rawr_resources( 51 | &self, 52 | limit: Option, 53 | sort: Option, 54 | ) -> HttpResult> { 55 | let mut request = self 56 | .client 57 | .request_route(&self.instance_url, LIST_UPLOADED_RESOURCES)? 58 | .bearer_auth(&self.token); 59 | if let Some(limit) = limit { 60 | request = request.query(&[("limit", &limit.to_string())]); 61 | } 62 | if let Some(sort) = sort { 63 | request = request.query(&[("sort", &sort.to_string())]); 64 | } 65 | let response = request.send().await?; 66 | let status = response.status(); 67 | let response_text = response.text().await?; 68 | if status == StatusCode::NO_CONTENT || response_text.is_empty() { 69 | Ok(Vec::new()) 70 | } else { 71 | serde_json::from_str::>(&response_text) 72 | .map_err(RequestError::from) 73 | } 74 | } 75 | 76 | /// Replace the access properties of a `RawR` resource with updated access properties. 77 | pub async fn update_rawr_resource_access( 78 | &self, 79 | rid: &str, 80 | new_access_properties: ResourceAccessProperties, 81 | ) -> HttpResult<()> { 82 | let request = self 83 | .client 84 | .client 85 | .request( 86 | UPDATE_RESOURCE_ACCESS.method, 87 | self.instance_url 88 | .join(UPDATE_RESOURCE_ACCESS.path)? 89 | .join(rid)?, 90 | ) 91 | .body(json!(new_access_properties).to_string()) 92 | .bearer_auth(&self.token); 93 | let response = request.send().await?; 94 | matches_status_code(&[StatusCode::NO_CONTENT, StatusCode::OK], response.status()) 95 | } 96 | 97 | /// Upload a `RawR` resource to your home server. 98 | pub async fn upload_rawr_resource( 99 | &self, 100 | resource: Resource, 101 | rid: &str, 102 | resource_access_properties: ResourceAccessProperties, 103 | ) -> HttpResult<()> { 104 | let request = self 105 | .client 106 | .client 107 | .request( 108 | UPDATE_RESOURCE_ACCESS.method, 109 | self.instance_url 110 | .join(UPDATE_RESOURCE_ACCESS.path)? 111 | .join(rid)?, 112 | ) 113 | .query(&[( 114 | "resourceAccessProperties", 115 | urlencoding::encode(&json!(resource_access_properties).to_string()), 116 | )]) 117 | .bearer_auth(&self.token) 118 | .header("Content-Length", resource.contents.len()) 119 | .multipart( 120 | resource 121 | .into_multipart() 122 | .map_err(|e| RequestError::Custom { 123 | reason: e.to_string(), 124 | })?, 125 | ); 126 | let response = request.send().await?; 127 | matches_status_code( 128 | &[ 129 | StatusCode::NO_CONTENT, 130 | StatusCode::OK, 131 | StatusCode::ACCEPTED, 132 | StatusCode::CREATED, 133 | ], 134 | response.status(), 135 | ) 136 | } 137 | 138 | /// Delete a resource by its resource id (rid). 139 | pub async fn delete_rawr_resource(&self, rid: &str) -> HttpResult<()> { 140 | let request = self 141 | .client 142 | .client 143 | .request( 144 | DELETE_RESOURCE.method, 145 | self.instance_url.join(DELETE_RESOURCE.path)?.join(rid)?, 146 | ) 147 | .bearer_auth(&self.token); 148 | let response = request.send().await?; 149 | matches_status_code(&[StatusCode::NO_CONTENT, StatusCode::OK], response.status()) 150 | } 151 | } 152 | } 153 | 154 | mod registration_not_required { 155 | use crate::types::ResourceInformation; 156 | 157 | use super::*; 158 | 159 | impl> Session {} 160 | 161 | impl HttpClient { 162 | /// Retrieve a `RawR` resource by specifying the ID (`rid`) of the resource. 163 | /// 164 | /// ## Parameters 165 | /// 166 | /// - `host`: The URL of the polyproto instance to query 167 | pub async fn get_rawr_resource_by_id( 168 | &self, 169 | rid: &str, 170 | token: Option, 171 | host: &Url, 172 | ) -> HttpResult> { 173 | let mut request = self.client.request( 174 | GET_RESOURCE_BY_ID.method, 175 | host.join(GET_RESOURCE_BY_ID.path)?.join(rid)?, 176 | ); 177 | if let Some(token) = token { 178 | request = request.bearer_auth(token); 179 | } 180 | let response = request.send().await?; 181 | Ok(response.bytes().await?.to_vec()) 182 | } 183 | 184 | /// Retrieve [ResourceInformation] about a `RawR` resource. 185 | /// ## Parameters 186 | /// 187 | /// - `host`: The URL of the polyproto instance to query 188 | pub async fn get_rawr_resource_info_by_id( 189 | &self, 190 | host: &Url, 191 | rid: &str, 192 | token: Option, 193 | ) -> HttpResult { 194 | let mut request = self.client.request( 195 | GET_RESOURCE_INFO_BY_ID.method, 196 | host.join(&GET_RESOURCE_INFO_BY_ID.path.replace(r#"{rid}"#, rid))?, 197 | ); 198 | if let Some(token) = token { 199 | request = request.bearer_auth(token); 200 | } 201 | let response = request.send().await; 202 | // BUG: Will error if the list is empty/204 is received. 203 | HttpClient::handle_response(response).await 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/api/core/services.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use super::*; 6 | mod registration_required { 7 | use serde_json::json; 8 | 9 | use super::*; 10 | 11 | impl> Session { 12 | /// Add a service to the list of discoverable services. The service must be a valid [Service]. 13 | /// If the service provider is the first to provide this service, or if the [Service] has a 14 | /// property of `primary` set to `true`, the service will be marked as the primary service 15 | /// provider for this service. 16 | /// 17 | /// The server will return a [Vec] of all [Service]s affected 18 | /// by this operation. This [Vec] will have a length of 1, if no other service entry was 19 | /// affected, and a length of 2 if this new service entry has replaced an existing one in the 20 | /// role of primary service provider. 21 | pub async fn add_discoverable_service( 22 | &self, 23 | service: &Service, 24 | ) -> HttpResult> { 25 | let request = self 26 | .client 27 | .client 28 | .request( 29 | CREATE_DISCOVERABLE.method.clone(), 30 | self.instance_url.join(CREATE_DISCOVERABLE.path)?, 31 | ) 32 | .bearer_auth(&self.token) 33 | .body(json!(service).to_string()); 34 | let response = request.send().await; 35 | HttpClient::handle_response::>(response).await 36 | } 37 | 38 | /// Delete a discoverable service from the list of discoverable services. The service must be a 39 | /// valid [Service] that exists in the list of discoverable services. On success, the server will 40 | /// return a [ServiceDeleteResponse] containing the deleted service and, if applicable, the new 41 | /// primary service provider for the service. 42 | pub async fn delete_discoverable_service( 43 | &self, 44 | url: &Url, 45 | name: &ServiceName, 46 | ) -> HttpResult { 47 | let request = self 48 | .client 49 | .client 50 | .request( 51 | DELETE_DISCOVERABLE.method.clone(), 52 | self.instance_url.join(DELETE_DISCOVERABLE.path)?, 53 | ) 54 | .bearer_auth(&self.token) 55 | .body( 56 | json!({ 57 | "url": url, 58 | "name": name, 59 | }) 60 | .to_string(), 61 | ); 62 | let response = request.send().await; 63 | HttpClient::handle_response::(response).await 64 | } 65 | 66 | /// Set the primary service provider for a service, by specifying the URL of the new primary 67 | /// service provider and the name of the service. The server will return a [Vec] of all [Service]s 68 | /// affected by this operation. This [Vec] will have a length of 1, if no other service entry was 69 | /// affected, and a length of 2 if this new service entry has replaced an existing one in the 70 | /// role of primary service provider. 71 | pub async fn set_primary_service_provider( 72 | &self, 73 | url: &Url, 74 | name: &ServiceName, 75 | ) -> HttpResult> { 76 | let request = self 77 | .client 78 | .client 79 | .request( 80 | SET_PRIMARY_DISCOVERABLE.method.clone(), 81 | self.instance_url.join(SET_PRIMARY_DISCOVERABLE.path)?, 82 | ) 83 | .bearer_auth(&self.token) 84 | .body( 85 | json!({ 86 | "url": url, 87 | "name": name 88 | }) 89 | .to_string(), 90 | ); 91 | let response = request.send().await; 92 | HttpClient::handle_response::>(response).await 93 | } 94 | } 95 | } 96 | 97 | mod registration_not_required { 98 | use serde_json::json; 99 | 100 | use super::*; 101 | 102 | impl> Session {} 103 | 104 | impl HttpClient { 105 | /// Fetch a list of all services that the actor specified in the `actor_fid` argument has made 106 | /// discoverable. 107 | /// 108 | /// ## Parameters 109 | /// 110 | /// `limit`: How many results to return at maximum. Omitting this value will return all existing 111 | /// results. 112 | pub async fn discover_services( 113 | &self, 114 | actor_fid: &FederationId, 115 | limit: Option, 116 | instance_url: &Url, 117 | ) -> HttpResult> { 118 | let request_url = instance_url 119 | .join(DISCOVER_SERVICE_ALL.path)? 120 | .join(&actor_fid.to_string())?; 121 | let mut request = self 122 | .client 123 | .request(DISCOVER_SERVICE_ALL.method.clone(), request_url); 124 | if let Some(limit) = limit { 125 | request = request.body( 126 | json!({ 127 | "limit": limit 128 | }) 129 | .to_string(), 130 | ); 131 | } 132 | let response = request.send().await; 133 | HttpClient::handle_response::>(response).await 134 | } 135 | 136 | /// Fetch a list of services an actor is registered with, filtered by `service_name`. 137 | /// 138 | /// ## Parameters 139 | /// 140 | /// `limit`: Whether to limit the amount of returned results. Not specifying a limit will 141 | /// return all services. Specifying a limit value of 1 will return only the primary 142 | /// service provider. 143 | pub async fn discover_service( 144 | &self, 145 | actor_fid: &FederationId, 146 | service_name: &ServiceName, 147 | limit: Option, 148 | instance_url: &Url, 149 | ) -> HttpResult> { 150 | let request_url = 151 | instance_url.join(&format!("{}{}", DISCOVER_SERVICE_SINGULAR.path, actor_fid))?; 152 | let mut request = self 153 | .client 154 | .request(DISCOVER_SERVICE_SINGULAR.method.clone(), request_url); 155 | if let Some(limit) = limit { 156 | request = request.body( 157 | json!({ 158 | "limit": limit, 159 | "name": service_name 160 | }) 161 | .to_string(), 162 | ); 163 | } else { 164 | request = request.body( 165 | json!({ 166 | "name": service_name 167 | }) 168 | .to_string(), 169 | ); 170 | } 171 | let response = request.send().await; 172 | HttpClient::handle_response::>(response).await 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/certs/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::str::FromStr; 6 | 7 | use der::asn1::BitString; 8 | use der::pem::LineEnding; 9 | use der::{Decode, DecodePem, Encode, EncodePem}; 10 | use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; 11 | use x509_cert::name::{Name, RdnSequence}; 12 | 13 | use crate::errors::CertificateConversionError; 14 | use crate::types::der::asn1::Ia5String; 15 | use crate::{Constrained, ConstraintError, OID_RDN_DOMAIN_COMPONENT}; 16 | 17 | /// Additional capabilities ([x509_cert::ext::Extensions] or [x509_cert::attr::Attributes], depending 18 | /// on the context) of X.509 certificates. 19 | pub mod capabilities; 20 | /// Complete, signed [IdCert] 21 | pub mod idcert; 22 | /// [IdCertTbs] is an [IdCert] which has not yet been signed by 23 | pub mod idcerttbs; 24 | /// Certificate Signing Request for an [IdCert]/[IdCertTbs] 25 | pub mod idcsr; 26 | 27 | /// polyproto client Session ID. Must be unique for each client. Must be between 1 and =32 28 | /// characters in length. The session ID is used to uniquely identify a client in the context of 29 | /// polyproto. Client certificates will change over time, but the session ID of a particular client 30 | /// will remain the same. 31 | /// 32 | /// [Constrained] is implemented for this type, meaning it can be validated using `.validate()`. 33 | #[derive(Debug, Clone, PartialEq, Eq)] 34 | pub struct SessionId { 35 | /// The session ID, represented as an [Ia5String]. 36 | session_id: Ia5String, 37 | } 38 | 39 | impl std::fmt::Display for SessionId { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | self.session_id.fmt(f) 42 | } 43 | } 44 | 45 | impl SessionId { 46 | #[allow(clippy::new_ret_no_self)] 47 | /// Creates a new [SessionId] which can be converted into an [Attribute] using `.as_attribute()`, 48 | /// if needed. Checks if the input is a valid Ia5String and if the [SessionId] constraints have 49 | /// been violated. 50 | pub fn new_validated(id: &str) -> Result { 51 | let ia5string = match der::asn1::Ia5String::new(id) { 52 | Ok(string) => string, 53 | Err(_) => { 54 | return Err(ConstraintError::Malformed(Some( 55 | "Invalid Ia5String passed as SessionId".to_string(), 56 | ))); 57 | } 58 | }; 59 | 60 | let session_id = SessionId { 61 | session_id: ia5string.into(), 62 | }; 63 | session_id.validate(None)?; 64 | Ok(session_id) 65 | } 66 | 67 | /// Converts this [SessionId] into a [Name] for use in a certificate. 68 | pub fn to_rdn_sequence(&self) -> Name { 69 | RdnSequence::from_str(&format!("uniqueIdentifier={}", self)).unwrap() 70 | } 71 | 72 | /// Returns the inner [Ia5String] of this [SessionId] as an owned value. 73 | pub fn to_ia5string(&self) -> Ia5String { 74 | self.session_id.clone() 75 | } 76 | } 77 | 78 | impl From for Ia5String { 79 | fn from(value: SessionId) -> Self { 80 | value.session_id 81 | } 82 | } 83 | 84 | impl TryFrom for SessionId { 85 | type Error = ConstraintError; 86 | 87 | fn try_from(value: Ia5String) -> Result { 88 | SessionId::new_validated(value.to_string().as_str()) 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 93 | /// Whether something is intended for an actor or a home server. 94 | #[allow(missing_docs)] 95 | pub enum Target { 96 | Actor, 97 | HomeServer, 98 | } 99 | 100 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] 101 | #[repr(u8)] 102 | /// `PKCS#10` version. From the PKCS specification document (RFC 2986): 103 | /// > version is the version number, for compatibility with future 104 | /// > revisions of this document. It shall be 0 for this version of 105 | /// > the standard. 106 | /// 107 | /// The specification also says: 108 | /// > `version INTEGER { v1(0) } (v1,...),` 109 | /// 110 | /// Version "v1" corresponds to enum variant `V1`, which is represented as the `u8` 111 | /// integer zero (0). 112 | pub enum PkcsVersion { 113 | #[default] 114 | /// Version 1 (0) of the PKCS#10 specification implementation 115 | V1 = 0, 116 | } 117 | 118 | /// Information regarding a subjects' public key. This is a `SubjectPublicKeyInfo` in the context of 119 | /// PKCS #10. 120 | #[derive(Debug, PartialEq, Eq, Clone)] 121 | pub struct PublicKeyInfo { 122 | /// Properties of the signature algorithm used to create the public key. 123 | pub algorithm: AlgorithmIdentifierOwned, 124 | /// The public key, represented as a [BitString]. 125 | pub public_key_bitstring: BitString, 126 | } 127 | 128 | impl PublicKeyInfo { 129 | /// Create a new [PublicKeyInfo] from the provided DER encoded data. The data must be a valid, 130 | /// DER encoded PKCS #10 `SubjectPublicKeyInfo` structure. The caller is responsible for 131 | /// verifying the correctness of the resulting data before using it. 132 | pub fn from_der(value: &str) -> Result { 133 | Ok(SubjectPublicKeyInfoOwned::from_der(value.as_bytes())?.into()) 134 | } 135 | 136 | /// Create a new [PublicKeyInfo] from the provided PEM encoded data. The data must be a valid, 137 | /// PEM encoded PKCS #10 `SubjectPublicKeyInfo` structure. The caller is responsible for 138 | /// verifying the correctness of the resulting data before using it. 139 | pub fn from_pem(value: &str) -> Result { 140 | Ok(SubjectPublicKeyInfoOwned::from_pem(value.as_bytes())?.into()) 141 | } 142 | 143 | /// Encode this type as DER, returning a byte vector. 144 | pub fn to_der(&self) -> Result, CertificateConversionError> { 145 | Ok(SubjectPublicKeyInfoOwned::from(self.clone()).to_der()?) 146 | } 147 | 148 | /// Encode this type as PEM, returning a string. 149 | pub fn to_pem(&self, line_ending: LineEnding) -> Result { 150 | Ok(SubjectPublicKeyInfoOwned::from(self.clone()).to_pem(line_ending)?) 151 | } 152 | } 153 | 154 | impl From for PublicKeyInfo { 155 | fn from(value: SubjectPublicKeyInfoOwned) -> Self { 156 | PublicKeyInfo { 157 | algorithm: value.algorithm, 158 | public_key_bitstring: value.subject_public_key, 159 | } 160 | } 161 | } 162 | 163 | impl From for SubjectPublicKeyInfoOwned { 164 | fn from(value: PublicKeyInfo) -> Self { 165 | SubjectPublicKeyInfoOwned { 166 | algorithm: value.algorithm, 167 | subject_public_key: value.public_key_bitstring, 168 | } 169 | } 170 | } 171 | 172 | /// Checks, if the domain components of two [Name]s are equal and ordered in the same way. Returns 173 | /// `true`, if the domain components are equal, `false` otherwise. 174 | pub fn equal_domain_components(name_1: &Name, name_2: &Name) -> bool { 175 | let mut domain_components_1 = Vec::new(); 176 | let mut domain_components_2 = Vec::new(); 177 | for rdn in name_1.0.iter() { 178 | for ava in rdn.0.iter() { 179 | if ava.oid.to_string().as_str() == OID_RDN_DOMAIN_COMPONENT { 180 | domain_components_1.push(String::from_utf8_lossy(ava.value.value())); 181 | } 182 | } 183 | } 184 | for rdn in name_2.0.iter() { 185 | for ava in rdn.0.iter() { 186 | if ava.oid.to_string().as_str() == OID_RDN_DOMAIN_COMPONENT { 187 | domain_components_2.push(String::from_utf8_lossy(ava.value.value())); 188 | } 189 | } 190 | } 191 | domain_components_1 == domain_components_2 192 | } 193 | 194 | #[cfg(test)] 195 | mod test { 196 | use std::str::FromStr; 197 | 198 | use x509_cert::name::RdnSequence; 199 | 200 | use super::*; 201 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 202 | #[cfg_attr(not(target_arch = "wasm32"), test)] 203 | fn test_equal_domain_components_eq() { 204 | #[allow(clippy::unwrap_used)] 205 | let rdn_1 = RdnSequence::from_str( 206 | "CN=root,OU=programmer,DC=www,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", 207 | ) 208 | .unwrap(); 209 | 210 | #[allow(clippy::unwrap_used)] 211 | let rdn_2 = RdnSequence::from_str( 212 | "CN=user1,DC=www,DC=polyphony,DC=chat,UID=user1@polyphony.chat,uniqueIdentifier=user1", 213 | ) 214 | .unwrap(); 215 | assert!(equal_domain_components(&rdn_1, &rdn_2)); 216 | } 217 | 218 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 219 | #[cfg_attr(not(target_arch = "wasm32"), test)] 220 | fn test_equal_domain_components_ne() { 221 | #[allow(clippy::unwrap_used)] 222 | let rdn_1 = RdnSequence::from_str( 223 | "CN=root,OU=programmer,DC=www,DC=polyphony,DC=chat,UID=root@polyphony.chat,uniqueIdentifier=root", 224 | ) 225 | .unwrap(); 226 | 227 | #[allow(clippy::unwrap_used)] 228 | let rdn_2 = RdnSequence::from_str( 229 | "CN=user1,DC=proto,DC=polyphony,DC=chat,UID=user1@polyphony.chat,uniqueIdentifier=user1", 230 | ) 231 | .unwrap(); 232 | assert!(!equal_domain_components(&rdn_1, &rdn_2)); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/constraints/capabilities.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::errors::{ERR_MSG_ACTOR_MISSING_SIGNING_CAPS, ERR_MSG_HOME_SERVER_MISSING_CA_ATTR}; 6 | 7 | use super::*; 8 | 9 | impl Constrained for Capabilities { 10 | fn validate(&self, _target: Option) -> Result<(), ConstraintError> { 11 | let is_ca = self.basic_constraints.ca; 12 | 13 | // Define the flags to check 14 | let mut can_commit_content = false; 15 | let mut can_sign = false; 16 | let mut key_cert_sign = false; 17 | let mut has_only_encipher = false; 18 | let mut has_only_decipher = false; 19 | let mut has_key_agreement = false; 20 | 21 | // Iterate over all the entries in the KeyUsage vector, check if they exist/are true 22 | for item in self.key_usage.key_usages.iter() { 23 | if !has_only_encipher && item == &KeyUsage::EncipherOnly { 24 | has_only_encipher = true; 25 | } 26 | if !has_only_decipher && item == &KeyUsage::DecipherOnly { 27 | has_only_decipher = true; 28 | } 29 | if !has_key_agreement && item == &KeyUsage::KeyAgreement { 30 | has_key_agreement = true; 31 | } 32 | if !has_key_agreement && item == &KeyUsage::ContentCommitment { 33 | can_commit_content = true; 34 | } 35 | if !has_key_agreement && item == &KeyUsage::DigitalSignature { 36 | can_sign = true; 37 | } 38 | if !has_key_agreement && item == &KeyUsage::KeyCertSign { 39 | key_cert_sign = true; 40 | } 41 | } 42 | 43 | // Non-CAs must be able to sign their messages. Whether with or without non-repudiation 44 | // does not matter. 45 | if !is_ca && !can_sign && !can_commit_content { 46 | return Err(ConstraintError::Malformed(Some( 47 | ERR_MSG_ACTOR_MISSING_SIGNING_CAPS.to_string(), 48 | ))); 49 | } 50 | 51 | // Certificates cannot be both non-repudiating and repudiating 52 | if can_sign && can_commit_content { 53 | return Err(ConstraintError::Malformed(Some( 54 | "Cannot have both signing and non-repudiation signing capabilities".to_string(), 55 | ))); 56 | } 57 | 58 | // If these Capabilities are for a CA, it also must have the KeyCertSign Capability set to 59 | // true. Also, non-CAs are not allowed to have the KeyCertSign flag set to true. 60 | if is_ca || key_cert_sign { 61 | if !is_ca { 62 | return Err(ConstraintError::Malformed(Some( 63 | "If KeyCertSign capability is wanted, CA flag must be true".to_string(), 64 | ))); 65 | } 66 | if !key_cert_sign { 67 | return Err(ConstraintError::Malformed(Some(format!( 68 | "{} Missing capability \"KeyCertSign\"", 69 | ERR_MSG_HOME_SERVER_MISSING_CA_ATTR 70 | )))); 71 | } 72 | } 73 | 74 | // has_key_agreement needs to be true if has_only_encipher or _decipher are true. 75 | // See: 76 | // See: 77 | if (has_only_encipher || has_only_decipher) && !has_key_agreement { 78 | Err(ConstraintError::Malformed(Some( 79 | "KeyAgreement capability needs to be true to use OnlyEncipher or OnlyDecipher" 80 | .to_string(), 81 | ))) 82 | } else { 83 | Ok(()) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/constraints/certs.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use log::{debug, warn}; 6 | 7 | use crate::errors::{ 8 | ERR_MSG_ACTOR_CANNOT_BE_CA, ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, 9 | ERR_MSG_HOME_SERVER_MISSING_CA_ATTR, ERR_MSG_SIGNATURE_MISMATCH, 10 | }; 11 | 12 | use super::*; 13 | 14 | impl> Constrained for IdCsrInner { 15 | fn validate(&self, target: Option) -> Result<(), ConstraintError> { 16 | log::trace!( 17 | "[IdCsrInner::validate()] validating capabilities for target: {:?}", 18 | target 19 | ); 20 | self.capabilities.validate(target)?; 21 | log::trace!( 22 | "[IdCsrInner::validate()] validating subject for target: {:?}", 23 | target 24 | ); 25 | self.subject.validate(target)?; 26 | if let Some(target) = target { 27 | match target { 28 | Target::Actor => { 29 | if self.capabilities.basic_constraints.ca { 30 | return Err(ConstraintError::Malformed(Some( 31 | ERR_MSG_ACTOR_CANNOT_BE_CA.to_string(), 32 | ))); 33 | } 34 | } 35 | Target::HomeServer => { 36 | if !self.capabilities.basic_constraints.ca { 37 | return Err(ConstraintError::Malformed(Some( 38 | ERR_MSG_HOME_SERVER_MISSING_CA_ATTR.to_string(), 39 | ))); 40 | } 41 | } 42 | } 43 | } 44 | Ok(()) 45 | } 46 | } 47 | 48 | impl> Constrained for IdCsr { 49 | fn validate(&self, target: Option) -> Result<(), ConstraintError> { 50 | log::trace!( 51 | "[IdCsr::validate()] validating inner CSR with target {:?}", 52 | target 53 | ); 54 | self.inner_csr.validate(target)?; 55 | log::trace!("[IdCsr::validate()] verifying signature"); 56 | match self.inner_csr.subject_public_key.verify_signature( 57 | &self.signature, 58 | match &self.inner_csr.clone().to_der() { 59 | Ok(data) => data, 60 | Err(_) => { 61 | log::warn!("[IdCsr::validate()] DER conversion failure when converting inner IdCsr to DER. IdCsr is likely malformed"); 62 | return Err(ConstraintError::Malformed(Some("DER conversion failure when converting inner IdCsr to DER. IdCsr is likely malformed".to_string())))} 63 | } 64 | ) { 65 | Ok(_) => (), 66 | Err(_) => { 67 | log::warn!( 68 | "[IdCsr::validate()] {}", ERR_MSG_SIGNATURE_MISMATCH); 69 | return Err(ConstraintError::Malformed(Some(ERR_MSG_SIGNATURE_MISMATCH.to_string())))} 70 | }; 71 | Ok(()) 72 | } 73 | } 74 | 75 | impl> Constrained for IdCert { 76 | fn validate(&self, target: Option) -> Result<(), ConstraintError> { 77 | log::trace!( 78 | "[IdCert::validate()] validating inner IdCertTbs with target {:?}", 79 | target 80 | ); 81 | self.id_cert_tbs.validate(target)?; 82 | Ok(()) 83 | } 84 | } 85 | 86 | impl> Constrained for IdCertTbs { 87 | fn validate(&self, target: Option) -> Result<(), ConstraintError> { 88 | log::trace!( 89 | "Validating if DER encoding is intact for certificate serial {:?}", 90 | self.serial_number 91 | ); 92 | match self.clone().to_der() { 93 | Ok(der) => der, 94 | Err(_) => { 95 | log::warn!("{}", crate::errors::ERR_CERTIFICATE_TO_DER_ERROR); 96 | return Err(ConstraintError::Malformed(Some( 97 | crate::errors::ERR_CERTIFICATE_TO_DER_ERROR.to_string(), 98 | ))); 99 | } 100 | }; 101 | log::trace!("validating capabilities for target: {:?}", target); 102 | self.capabilities.validate(target)?; 103 | self.issuer.validate(Some(Target::HomeServer))?; 104 | self.subject.validate(target)?; 105 | log::trace!("Checking if domain components of issuer and subject are equal"); 106 | log::trace!("Issuer: {}", self.issuer); 107 | log::trace!("Subject: {}", self.subject); 108 | match equal_domain_components(&self.issuer, &self.subject) { 109 | true => debug!("Domain components of issuer and subject are equal"), 110 | false => { 111 | warn!( 112 | "{}\nIssuer: {}\nSubject: {}", 113 | ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, &self.issuer, &self.subject 114 | ); 115 | return Err(ConstraintError::Malformed(Some(format!( 116 | "{}\nIssuer: {}\nSubject: {}", 117 | ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT, &self.issuer, &self.subject 118 | )))); 119 | } 120 | } 121 | if let Some(target) = target { 122 | match target { 123 | Target::Actor => { 124 | if self.capabilities.basic_constraints.ca { 125 | return Err(ConstraintError::Malformed(Some( 126 | ERR_MSG_ACTOR_CANNOT_BE_CA.to_string(), 127 | ))); 128 | } 129 | } 130 | Target::HomeServer => { 131 | if !self.capabilities.basic_constraints.ca { 132 | return Err(ConstraintError::Malformed(Some( 133 | ERR_MSG_HOME_SERVER_MISSING_CA_ATTR.to_string(), 134 | ))); 135 | } 136 | } 137 | } 138 | } 139 | Ok(()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/constraints/session_id.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use super::*; 6 | 7 | impl Constrained for SessionId { 8 | /// [SessionId] must be longer than 0 and not longer than 32 characters to be deemed valid. 9 | fn validate(&self, _target: Option) -> Result<(), ConstraintError> { 10 | let len = self.to_ia5string().len(); 11 | if len > Length::new(32) || len == Length::ZERO { 12 | return Err(ConstraintError::OutOfBounds { 13 | lower: 1, 14 | upper: 32, 15 | actual: len.to_string(), 16 | reason: "SessionId too long".to_string(), 17 | }); 18 | } 19 | Ok(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/constraints/types/federation_id.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use regex::Regex; 6 | 7 | use crate::errors::ERR_MSG_FEDERATION_ID_REGEX; 8 | use crate::types::{FederationId, REGEX_FEDERATION_ID}; 9 | 10 | use super::*; 11 | 12 | impl Constrained for FederationId { 13 | fn validate(&self, _target: Option) -> Result<(), ConstraintError> { 14 | if self.to_string().trim() != self.to_string().as_str() { 15 | return Err(ConstraintError::Malformed(Some(format!( 16 | "FederationId must not contain leading or trailing whitespace: {self}" 17 | )))); 18 | } 19 | let fid_regex = Regex::new(REGEX_FEDERATION_ID).unwrap(); 20 | match fid_regex.is_match(&self.to_string()) { 21 | true => Ok(()), 22 | false => Err(ConstraintError::Malformed(Some( 23 | ERR_MSG_FEDERATION_ID_REGEX.to_string(), 24 | ))), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/constraints/types/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | mod federation_id; 6 | mod rawr; 7 | mod service; 8 | 9 | use crate::Constrained; 10 | use crate::certs::Target; 11 | use crate::errors::ConstraintError; 12 | -------------------------------------------------------------------------------- /src/constraints/types/rawr.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::Constrained; 6 | use crate::types::ResourceAccessProperties; 7 | 8 | impl Constrained for ResourceAccessProperties { 9 | fn validate( 10 | &self, 11 | _target: Option, 12 | ) -> Result<(), crate::errors::ConstraintError> { 13 | if self.private && self.public { 14 | Err(crate::errors::ConstraintError::Malformed(Some( 15 | "A resource must not be marked as private AND public".to_string(), 16 | ))) 17 | } else { 18 | Ok(()) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/constraints/types/service.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::types::{Service, ServiceName}; 6 | 7 | use super::*; 8 | 9 | impl Constrained for Service { 10 | fn validate(&self, target: Option) -> Result<(), ConstraintError> { 11 | self.service.validate(target) 12 | } 13 | } 14 | 15 | impl Constrained for ServiceName { 16 | fn validate(&self, _target: Option) -> Result<(), ConstraintError> { 17 | let stringified = self.to_string(); 18 | if stringified.len() < 2 || stringified.len() > 64 { 19 | return Err(ConstraintError::OutOfBounds { 20 | lower: 2, 21 | upper: 64, 22 | actual: stringified.len().to_string(), 23 | reason: "The length of the ServiceName is outside of the allowed bounds" 24 | .to_string(), 25 | }); 26 | } 27 | let regex = 28 | regex::Regex::new(r"[^[:lower:][:digit:]\-_]").expect("Failed to compile regex!"); 29 | if regex.is_match(&stringified) { 30 | return Err(ConstraintError::Malformed(Some(format!( 31 | "The ServiceName contains invalid characters: \"{}\" contains characters that are not lowercase letters, digits, hyphens, or underscores", 32 | stringified 33 | )))); 34 | } 35 | Ok(()) 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod test { 41 | use super::*; 42 | 43 | #[test] 44 | fn valid_service_names() { 45 | ServiceName::new("example").unwrap(); 46 | ServiceName::new("example-1").unwrap(); 47 | ServiceName::new("example_1").unwrap(); 48 | ServiceName::new("example-1_2").unwrap(); 49 | ServiceName::new("example-1_2-3").unwrap(); 50 | ServiceName::new("e-x--a--___-m-----ple-1_2-3_4").unwrap(); 51 | ServiceName::new("abcdefghijklmnopqrstuvwxyz_-0123456789").unwrap(); 52 | } 53 | 54 | #[test] 55 | fn space_in_service_name() { 56 | assert!(ServiceName::new("example 1").is_err()); 57 | } 58 | 59 | #[test] 60 | fn non_lowercase_characters_in_service_name() { 61 | assert!(ServiceName::new("Example").is_err()); 62 | assert!(ServiceName::new("EXAMPLE").is_err()); 63 | assert!(ServiceName::new("exAmple").is_err()); 64 | assert!(ServiceName::new("exaMple").is_err()); 65 | assert!(ServiceName::new("exampLe").is_err()); 66 | } 67 | 68 | #[allow(clippy::invisible_characters)] 69 | #[test] 70 | fn non_latin_alphabet_characters_in_service_name() { 71 | assert!(ServiceName::new("🦀∄∄").is_err()); 72 | assert!(ServiceName::new("🦀🦀🦀").is_err()); 73 | assert!(ServiceName::new("∄∄∄").is_err()); 74 | assert!(ServiceName::new("#cool_name").is_err()); 75 | assert!(ServiceName::new("cool_name.").is_err()); 76 | // Between the letters "l" and "n", there is a zero-width space character (U+200B). 77 | assert!(ServiceName::new("cool​name").is_err()); 78 | } 79 | 80 | #[test] 81 | fn service_name_too_short() { 82 | assert!(ServiceName::new("a").is_err()); 83 | assert!(ServiceName::new("aa").is_ok()); 84 | } 85 | 86 | #[test] 87 | fn service_name_too_long() { 88 | assert!( 89 | ServiceName::new("12345678123456781234567812345678123456781234567812345678123456789") 90 | .is_err() 91 | ); 92 | assert!( 93 | ServiceName::new("1234567812345678123456781234567812345678123456781234567812345678") 94 | .is_ok() 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/errors/base.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use thiserror::Error; 6 | 7 | #[derive(Error, Debug, PartialEq, Clone)] 8 | /// Constraint validation errors. 9 | pub enum ConstraintError { 10 | #[error("The value did not meet the set validation criteria and is considered malformed")] 11 | /// The value did not meet the set validation criteria and is considered malformed 12 | Malformed(Option), 13 | #[error("The value was expected to be between {lower:?} and {upper:?} but was {actual:?}")] 14 | /// A value is out of bounds 15 | OutOfBounds { 16 | /// The lower bound of the value 17 | lower: i32, 18 | /// The upper bound of the value 19 | upper: i32, 20 | /// The actual value 21 | actual: String, 22 | /// Additional context 23 | reason: String, 24 | }, 25 | } 26 | 27 | /// Represents errors for invalid input. Differs from [ConstraintError], in that `ConstraintError` is 28 | /// only used on types implementing the [crate::Constrained] trait. 29 | #[derive(Error, Debug, PartialEq, Clone)] 30 | pub enum InvalidInput { 31 | #[error("The value is malformed and cannot be used as input: {0}")] 32 | /// The value is malformed and cannot be used as input 33 | Malformed(String), 34 | #[error( 35 | "The value was expected to be between {min_length:?} and {max_length:?} but was {actual_length:?}" 36 | )] 37 | /// A value is out of bounds 38 | Length { 39 | /// The minimum length of the value 40 | min_length: usize, 41 | /// The maximum length of the value 42 | max_length: usize, 43 | /// The actual length of the value 44 | actual_length: String, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /src/errors/composite.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use spki::ObjectIdentifier; 6 | use thiserror::Error; 7 | 8 | use super::base::{ConstraintError, InvalidInput}; 9 | 10 | #[derive(Error, Debug, PartialEq, Clone)] 11 | /// Errors that can occur when validating a certificate 12 | pub enum InvalidCert { 13 | #[error(transparent)] 14 | /// Signature or public key are invalid 15 | PublicKeyError(#[from] PublicKeyError), 16 | #[error(transparent)] 17 | /// The certificate does not pass validation of polyproto constraints 18 | InvalidProperties(#[from] ConstraintError), 19 | #[error("The validity period of the certificate is invalid, or the certificate is expired")] 20 | /// The certificate is expired or has an invalid validity period 21 | InvalidValidity, 22 | } 23 | 24 | #[derive(Error, Debug, PartialEq, Hash, Clone, Copy)] 25 | /// Errors related to Public Keys and Signatures 26 | pub enum PublicKeyError { 27 | #[error("The signature does not match the data")] 28 | /// The signature does not match the data or the signature is malformed 29 | BadSignature, 30 | #[error("The provided PublicKeyInfo could not be made into a PublicKey")] 31 | /// The provided PublicKey is invalid 32 | BadPublicKeyInfo, 33 | } 34 | 35 | #[derive(Error, Debug, PartialEq, Clone)] 36 | /// Errors that can occur when converting between certificate-related types 37 | pub enum CertificateConversionError { 38 | #[error(transparent)] 39 | /// The constraints of the source or target types were met 40 | ConstraintError(#[from] ConstraintError), 41 | #[error(transparent)] 42 | /// The input was invalid - Either malformed or out of bounds 43 | InvalidInput(#[from] InvalidInput), 44 | #[error("Encountered DER encoding error")] 45 | /// An error occurred while parsing a DER encoded object 46 | DerError(der::Error), 47 | #[error("Encountered DER OID error")] 48 | /// An error occurred while parsing an OID 49 | ConstOidError(der::oid::Error), 50 | #[error("Critical extension cannot be converted")] 51 | /// A critical extension is unknown and cannot be converted 52 | UnknownCriticalExtension { 53 | /// The OID of the unknown extension 54 | oid: ObjectIdentifier, 55 | }, 56 | #[error(transparent)] 57 | /// The source or target certificate is invalid 58 | InvalidCert(#[from] InvalidCert), 59 | } 60 | #[cfg(feature = "reqwest")] 61 | #[derive(Error, Debug)] 62 | /// Errors that can occur when making a request 63 | pub enum RequestError { 64 | #[error(transparent)] 65 | /// Reqwest encountered an error 66 | HttpError(#[from] reqwest::Error), 67 | #[error("Failed to deserialize response into expected type")] 68 | /// The response could not be deserialized into the expected type 69 | DeserializationError(#[from] serde_json::Error), 70 | #[error("Failed to convert response into expected type")] 71 | /// The response could not be converted into the expected type 72 | ConversionError(#[from] CertificateConversionError), 73 | #[error(transparent)] 74 | /// The URL could not be parsed 75 | UrlError(#[from] url::ParseError), 76 | /// Received a status code that indicates something other than success. 77 | #[error("Received status code {:?}, expected any of {:?}", received, expected)] 78 | StatusCode { 79 | received: http::StatusCode, 80 | expected: Vec, 81 | }, 82 | #[error("{reason}")] 83 | Custom { reason: String }, 84 | } 85 | 86 | #[cfg(feature = "reqwest")] 87 | impl From for RequestError { 88 | fn from(value: InvalidInput) -> Self { 89 | Self::Custom { 90 | reason: value.to_string(), 91 | } 92 | } 93 | } 94 | 95 | impl From for CertificateConversionError { 96 | fn from(value: der::Error) -> Self { 97 | Self::DerError(value) 98 | } 99 | } 100 | 101 | impl From for CertificateConversionError { 102 | fn from(value: der::oid::Error) -> Self { 103 | Self::ConstOidError(value) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #![allow(missing_docs)] 6 | 7 | pub static ERR_MSG_HOME_SERVER_MISSING_CA_ATTR: &str = 8 | "Home servers CSRs and Certificates must have the \"CA\" capability set to true!"; 9 | pub static ERR_MSG_ACTOR_CANNOT_BE_CA: &str = 10 | "Actor CSRs and Certificates must not have \"CA\" capabilities!"; 11 | pub static ERR_MSG_SIGNATURE_MISMATCH: &str = 12 | "Provided signature does not match computed signature!"; 13 | pub static ERR_MSG_ACTOR_MISSING_SIGNING_CAPS: &str = "Actors require one of the following capabilities: \"DigitalSignature\", \"ContentCommitment\". None provided."; 14 | pub static ERR_MSG_DC_UID_MISMATCH: &str = 15 | "The domain components found in the DC and UID fields of the Name object do not match!"; 16 | pub static ERR_MSG_DC_MISMATCH_ISSUER_SUBJECT: &str = 17 | "The domain components of the issuer and the subject do not match!"; 18 | pub static ERR_CERTIFICATE_TO_DER_ERROR: &str = 19 | "The certificate seems to be malformed, as it cannot be converted to DER."; 20 | #[cfg(feature = "types")] 21 | pub static ERR_MSG_CHALLENGE_STRING_LENGTH: &str = 22 | "Challenge strings must be between 32 and 255 bytes long!"; 23 | #[cfg(feature = "types")] 24 | pub static ERR_MSG_FEDERATION_ID_REGEX: &str = 25 | "Federation IDs must match the regex: \\b([a-z0-9._%+-]+)@([a-z0-9-]+(\\.[a-z0-9-]+)*)"; 26 | #[cfg(feature = "types")] 27 | pub static ERR_MSG_DOMAIN_NAME_REGEX: &str = 28 | "DomainNames must match the regex: \\b([a-z0-9._%+-]+)@([a-z0-9-]+(\\.[a-z0-9-]+)*)$"; 29 | /// "Base" error types which can be combined into "composite" error types 30 | pub mod base; 31 | /// "Composite" error types which consist of one or more "base" error types 32 | pub mod composite; 33 | 34 | pub use base::*; 35 | pub use composite::*; 36 | -------------------------------------------------------------------------------- /src/gateway/backends/heartbeat.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::time::Duration; 6 | 7 | use log::{debug, trace, warn}; 8 | use serde_json::{from_str, json}; 9 | use tokio::select; 10 | use tokio::sync::watch; 11 | use tokio::task::JoinHandle; 12 | 13 | use crate::types::gateway::{AnyEvent, CoreEvent, Payload}; 14 | 15 | use super::super::KILL_LOG_MESSAGE; 16 | use super::{Closed, GatewayMessage}; 17 | 18 | #[derive(Debug)] 19 | /// Gateway backend agnostic implementation of a heartbeat processing task. Checks all incoming events 20 | /// for sequence numbers and adds them to its initial state for later processing when sending a heartbeat. 21 | /// Sends a heartbeat every `interval` millis, automatically responds to manual heartbeat requests and 22 | /// retries sending heartbeats up to 5 times in case of an unstable connection. 23 | pub(crate) struct Heartbeat { 24 | _task_handle: JoinHandle<()>, 25 | } 26 | 27 | impl Heartbeat { 28 | pub(crate) fn spawn( 29 | mut kill_receive: watch::Receiver, 30 | kill_send: watch::Sender, 31 | mut message_receiver: watch::Receiver, 32 | message_sender: watch::Sender, 33 | interval: u32, 34 | ) -> Self { 35 | trace!("Heartbeat task spawned with interval of {interval}!"); 36 | let task_handle = tokio::spawn(async move { 37 | let mut _sleep = Box::pin(tokio::time::sleep(Duration::from_secs(interval as u64))); 38 | let mut received_sequences = Vec::::new(); 39 | loop { 40 | select! { 41 | _ = &mut _sleep => { 42 | trace!("Time to send another heartbeat!"); 43 | _sleep = Box::pin(tokio::time::sleep(Duration::from_secs(interval as u64))); 44 | received_sequences.dedup(); 45 | Self::try_send_heartbeat(message_sender.clone(), &received_sequences, kill_send.clone(), 1).await; 46 | received_sequences.shrink_to_fit(); 47 | continue; 48 | } 49 | _ = kill_receive.changed() => { 50 | trace!("{KILL_LOG_MESSAGE}"); 51 | kill_receive.borrow_and_update(); 52 | break; 53 | } 54 | _ = message_receiver.changed() => { 55 | let message = message_receiver.borrow_and_update().clone(); 56 | let message = match message { 57 | GatewayMessage::Text(t) => t, 58 | _ => continue // Non-text messages cannot be gateway payloads relevant to this code, so we skip 59 | }; 60 | let any_payload = match from_str::(&message) { 61 | Ok(payload) => payload, 62 | Err(_) => { 63 | trace!("This payload is not a valid AnyEvent, ignoring it"); 64 | continue 65 | }, 66 | }; 67 | trace!("Received AnyEvent {:?}", any_payload); 68 | if let Some(s) = any_payload.s { received_sequences.push(s) } 69 | let any_payload_namespace = { 70 | if any_payload.n.len() > 64 { 71 | warn!(r#"Received a payload with namespace "{}", which has a namespace of over 64 characters in length! This is technically not polyproto compliant (see section 8.2 of the protocol definition). In the future, such events might not be processed at all, or simply lead to an error."#, any_payload.n); 72 | "very_long_namespace".to_string() 73 | } else { 74 | any_payload.n.clone() 75 | } 76 | }; 77 | let any_payload_opcode = any_payload.op; 78 | let core_payload = match CoreEvent::try_from(any_payload) { 79 | Ok(p) => p, 80 | Err(e) => { 81 | debug!(r#"Payload with namespace "{}" and opcode {} does not seem to have valid CoreEvent data. Assuming it is not a manual heartbeat request and continuing."#, any_payload_namespace, any_payload_opcode); 82 | trace!("Actual error: {e}"); 83 | continue; 84 | } 85 | }; 86 | match core_payload.d() { 87 | crate::types::gateway::Payload::RequestHeartbeat => { 88 | trace!("Gateway server requested a manual heartbeat!"); 89 | received_sequences.dedup(); 90 | Self::try_send_heartbeat(message_sender.clone(), &received_sequences, kill_send.clone(), 1).await 91 | }, 92 | _ => continue 93 | }; 94 | } 95 | } 96 | } 97 | }); 98 | Self { 99 | _task_handle: task_handle, 100 | } 101 | } 102 | 103 | /// Attempts to send a heartbeat to the gateway. Will re-try sending the heartbeat up to 6 more times 104 | /// before giving up. Kills the gateway task when giving up, by sending a message through `kill_send`. 105 | async fn try_send_heartbeat( 106 | message_sender: watch::Sender, 107 | received_sequences: &Vec, 108 | kill_send: watch::Sender, 109 | attempt: u8, 110 | ) { 111 | if attempt > 5 { 112 | debug!("Tried sending heartbeat more than 5 times - never succeeded. Killing gateway"); 113 | match kill_send.send(Closed::Error("No response on heartbeat".to_string())) { 114 | Ok(_) => trace!("Sent kill signal successfully"), 115 | Err(kill_error) => trace!( 116 | "Sent kill signal, received error. Shutting down regardless: {kill_error}" 117 | ), 118 | }; 119 | return; 120 | } 121 | if attempt > 1 { 122 | trace!("Waiting before next attempt..."); 123 | tokio::time::sleep(Duration::from_millis(1500)).await; 124 | } 125 | let message = GatewayMessage::Text( 126 | json!(CoreEvent::new( 127 | Payload::Heartbeat(crate::types::gateway::payload::Heartbeat::from( 128 | received_sequences 129 | )), 130 | None 131 | )) 132 | .to_string(), 133 | ); 134 | let send_result = message_sender.send(message); 135 | trace!("Heartbeat send result: {:?}", send_result); 136 | match send_result { 137 | Ok(_) => trace!("Sent heartbeat!"), 138 | Err(e) => { 139 | debug!("Sending heartbeat to gateway failed, retrying: {e}"); 140 | Box::pin(Self::try_send_heartbeat( 141 | message_sender, 142 | received_sequences, 143 | kill_send, 144 | attempt + 1, 145 | )) 146 | .await 147 | } 148 | }; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/gateway/backends/wasm/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Backend {} 7 | -------------------------------------------------------------------------------- /src/gateway/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::fmt::Debug; 6 | use std::sync::Arc; 7 | 8 | use crate::api::Session; 9 | use crate::key::PrivateKey; 10 | use crate::signature::Signature; 11 | 12 | /// Gateway backend code. 13 | #[cfg(feature = "gateway")] 14 | pub mod backends; 15 | #[cfg(feature = "gateway")] 16 | pub use backends::BackendBehavior; 17 | #[cfg(feature = "gateway")] 18 | use backends::{Closed, GatewayMessage, heartbeat::Heartbeat}; 19 | use tokio::sync::watch; 20 | use tokio::task::JoinHandle; 21 | 22 | pub(crate) static KILL_LOG_MESSAGE: &str = "Received kill signal, shutting down"; 23 | 24 | #[derive(Debug)] 25 | /// A generic gateway, making no assumptions about the gateway backend in use. Communication is 26 | /// abstracted through the use of [tokio::sync::watch] channels, making sending and consumption of 27 | /// messages to/from a gateway server trivial. 28 | /// 29 | /// [Gateway] handles most of the gateway connection lifecycle. This includes building a connection, 30 | /// parsing [Hello] information and exhibiting proper [Heartbeat] behavior, including responding to 31 | /// [RequestHeartbeat] messages. 32 | pub struct Gateway> 33 | where 34 | S: Debug, 35 | >::PublicKey: Debug, 36 | { 37 | /// A reference to a corresponding [Session]. 38 | pub session: Arc>, 39 | /// This channel can be used to send [GatewayMessages](GatewayMessage) to the gateway server. 40 | pub send_channel: watch::Sender, 41 | /// This channel can be used to receive [GatewayMessages](GatewayMessage) from the gateway server. 42 | pub receive_channel: watch::Receiver, 43 | _kill_send: watch::Sender, 44 | /// Tokio task running "receiver" logic 45 | _receiver_task: JoinHandle<()>, 46 | /// Tokio task running "sender" logic 47 | _sender_task: JoinHandle<()>, 48 | /// Tokio task running heartbeat logic 49 | _heartbeat_task: Heartbeat, 50 | } 51 | 52 | /// Send a kill signal through a dedicated `watch` sender. 53 | /// 54 | /// ## Example 55 | /// 56 | /// ``` 57 | /// # use polyproto::gateway::backends::Closed; 58 | /// # use polyproto::gateway::kill; 59 | /// let sender = tokio::sync::watch::channel(Closed::Exhausted).0; 60 | /// // info is a `log::` loglevel macro, the last argument is the log message. 61 | /// kill!(sender, info, "We had to do it to 'em."); 62 | /// ``` 63 | #[macro_export] 64 | macro_rules! kill { 65 | ($kill_send:ident, $log_level:ident, $msg:expr) => {{ 66 | log::$log_level!("{}", $msg); 67 | match $kill_send.send(Closed::Error($msg.to_string())) { 68 | Ok(_) => log::trace!("Sent kill signal successfully"), 69 | Err(kill_error) => log::trace!( 70 | "Sent kill signal, received error. Shutting down regardless: {kill_error}" 71 | ), 72 | }; 73 | }}; 74 | } 75 | 76 | pub use kill; 77 | 78 | #[cfg(test)] 79 | mod test { 80 | use tokio::sync::watch; 81 | 82 | use crate::testing_utils::init_logger; 83 | 84 | use super::backends::Closed; 85 | 86 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 87 | #[cfg_attr(not(target_arch = "wasm32"), test)] 88 | fn kill_test() { 89 | init_logger(); 90 | let (send, receive) = watch::channel(Closed::Exhausted); 91 | kill!(send, info, "this is a test!"); 92 | assert!(receive.has_changed().unwrap()) 93 | } 94 | 95 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 96 | #[cfg_attr(not(target_arch = "wasm32"), test)] 97 | fn kill_test_cant_send() { 98 | init_logger(); 99 | let (send, receive) = watch::channel(Closed::Exhausted); 100 | drop(receive); 101 | kill!(send, info, "this is a test!"); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use spki::AlgorithmIdentifierOwned; 6 | 7 | use crate::certs::PublicKeyInfo; 8 | use crate::errors::{CertificateConversionError, PublicKeyError}; 9 | use crate::signature::Signature; 10 | 11 | /// A cryptographic private key generated by a [AlgorithmIdentifierOwned], with 12 | /// a corresponding [PublicKey] 13 | pub trait PrivateKey: PartialEq + Eq { 14 | /// The public key type corresponding to this private key. 15 | type PublicKey: PublicKey; 16 | /// Returns the public key corresponding to this private key. 17 | fn pubkey(&self) -> &Self::PublicKey; 18 | /// Creates a [Signature] for the given data. 19 | fn sign(&self, data: &[u8]) -> S; 20 | /// Returns the [AlgorithmIdentifierOwned] associated with this key's signature algorithm. 21 | fn algorithm_identifier(&self) -> AlgorithmIdentifierOwned { 22 | S::algorithm_identifier() 23 | } 24 | } 25 | 26 | /// A cryptographic public key generated by a [SignatureAlgorithm]. 27 | pub trait PublicKey: PartialEq + Eq + Clone { 28 | /// Verifies the correctness of a given [Signature] for a given piece of data. 29 | /// 30 | /// Implementations of this associated method should mitigate weak key forgery. 31 | fn verify_signature(&self, signature: &S, data: &[u8]) -> Result<(), PublicKeyError>; 32 | /// Returns the [PublicKeyInfo] associated with this key's signature algorithm. 33 | fn public_key_info(&self) -> PublicKeyInfo; 34 | /// Returns the [AlgorithmIdentifierOwned] associated with this key's signature algorithm. 35 | fn algorithm_identifier(&self) -> AlgorithmIdentifierOwned { 36 | S::algorithm_identifier() 37 | } 38 | /// Creates a new [Self] from a [PublicKeyInfo]. 39 | fn try_from_public_key_info( 40 | public_key_info: PublicKeyInfo, 41 | ) -> Result; 42 | } 43 | -------------------------------------------------------------------------------- /src/signature.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use spki::{AlgorithmIdentifierOwned, SignatureBitStringEncoding}; 6 | 7 | /// A signature value, generated using a [SignatureAlgorithm] 8 | pub trait Signature: PartialEq + Eq + SignatureBitStringEncoding + Clone + ToString { 9 | /// The underlying signature type 10 | type Signature; 11 | /// The signature value 12 | fn as_signature(&self) -> &Self::Signature; 13 | /// The [AlgorithmIdentifierOwned] associated with this signature 14 | fn algorithm_identifier() -> AlgorithmIdentifierOwned; 15 | /// From a byte slice, create a new [Self] 16 | fn from_bytes(signature: &[u8]) -> Self; 17 | /// Encode [Self] as a byte vector. 18 | fn as_bytes(&self) -> Vec; 19 | /// Encode [Self] as bytes, represented as a [hex](https://en.wikipedia.org/wiki/Hexadecimal)-encoded [String]. 20 | fn as_hex(&self) -> String { 21 | hex::encode(self.as_bytes()) 22 | } 23 | /// Try to decode [Self] from a [hex](https://en.wikipedia.org/wiki/Hexadecimal)-encoded String. 24 | /// 25 | /// ## Errors 26 | /// 27 | /// Will error if the input string is not valid hex-encoded data. 28 | fn try_from_hex(hex_encoded_bytes: &str) -> Result { 29 | Ok(Self::from_bytes( 30 | hex::decode(hex_encoded_bytes) 31 | .map_err(|e| crate::errors::InvalidInput::Malformed(e.to_string()))? 32 | .as_slice(), 33 | )) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/types/der/asn1/ia5string.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::ops::{Deref, DerefMut}; 6 | 7 | #[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)] 8 | /// Wrapper around [der::asn1::Ia5String], which provides serde support, if the `serde` feature is 9 | /// enabled. 10 | /// 11 | /// ASN.1 `IA5String` type. 12 | /// 13 | /// Supports the [International Alphabet No. 5 (IA5)] character encoding, i.e. 14 | /// the lower 128 characters of the ASCII alphabet. (Note: IA5 is now 15 | /// technically known as the International Reference Alphabet or IRA as 16 | /// specified in the ITU-T's T.50 recommendation). 17 | /// 18 | /// For UTF-8, use [`String`][`alloc::string::String`]. 19 | /// 20 | /// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_%28standard%29 21 | pub struct Ia5String(der::asn1::Ia5String); 22 | 23 | impl Ia5String { 24 | /// Create a new `IA5String`. 25 | pub fn new(input: &T) -> Result 26 | where 27 | T: AsRef<[u8]> + ?Sized, 28 | { 29 | Ok(Ia5String(der::asn1::Ia5String::new(input)?)) 30 | } 31 | } 32 | 33 | impl Deref for Ia5String { 34 | type Target = der::asn1::Ia5String; 35 | 36 | fn deref(&self) -> &Self::Target { 37 | &self.0 38 | } 39 | } 40 | 41 | impl DerefMut for Ia5String { 42 | fn deref_mut(&mut self) -> &mut Self::Target { 43 | &mut self.0 44 | } 45 | } 46 | 47 | impl From for Ia5String { 48 | fn from(s: der::asn1::Ia5String) -> Self { 49 | Self(s) 50 | } 51 | } 52 | 53 | impl From for der::asn1::Ia5String { 54 | fn from(s: Ia5String) -> Self { 55 | s.0 56 | } 57 | } 58 | 59 | #[cfg(feature = "serde")] 60 | mod serde_support { 61 | use super::Ia5String; 62 | use serde::de::Visitor; 63 | use serde::{Deserialize, Serialize}; 64 | 65 | impl<'de> Deserialize<'de> for Ia5String { 66 | fn deserialize(deserializer: D) -> Result 67 | where 68 | D: serde::Deserializer<'de>, 69 | { 70 | deserializer.deserialize_str(Ia5StringVisitor) 71 | } 72 | } 73 | 74 | struct Ia5StringVisitor; 75 | 76 | impl Visitor<'_> for Ia5StringVisitor { 77 | type Value = Ia5String; 78 | 79 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 80 | formatter.write_str( 81 | "a concatenation of characters from the IA5 character set in &str format", 82 | ) 83 | } 84 | 85 | fn visit_str(self, v: &str) -> Result 86 | where 87 | E: serde::de::Error, 88 | { 89 | Ok(Ia5String(match der::asn1::Ia5String::new(&v.to_string()) { 90 | Ok(val) => val, 91 | Err(e) => return Err(E::custom(e)), 92 | })) 93 | } 94 | } 95 | 96 | impl Serialize for Ia5String { 97 | fn serialize(&self, serializer: S) -> Result 98 | where 99 | S: serde::Serializer, 100 | { 101 | serializer.serialize_str(self.0.to_string().as_str()) 102 | } 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod test { 108 | use super::*; 109 | use serde_test::{Token, assert_de_tokens, assert_tokens}; 110 | 111 | #[test] 112 | fn ia5string_ser() { 113 | let ia5string = Ia5String(der::asn1::Ia5String::new("test").unwrap()); 114 | assert_tokens(&ia5string, &[Token::Str("test")]); 115 | let ia5string = Ia5String(der::asn1::Ia5String::new(&64u64.to_string()).unwrap()); 116 | assert_tokens(&ia5string, &[Token::Str("64")]); 117 | } 118 | 119 | #[test] 120 | fn ia5string_de() { 121 | let ia5string = Ia5String(der::asn1::Ia5String::new("test").unwrap()); 122 | assert_de_tokens(&ia5string, &[Token::Str("test")]); 123 | let ia5string = Ia5String(der::asn1::Ia5String::new(64u64.to_string().as_str()).unwrap()); 124 | assert_de_tokens(&ia5string, &[Token::Str("64")]); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/types/der/asn1/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #[allow(missing_docs)] 6 | pub mod ia5string; 7 | pub mod uint; 8 | 9 | pub use ia5string::*; 10 | pub use uint::*; 11 | -------------------------------------------------------------------------------- /src/types/der/asn1/uint.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::io::Read; 6 | use std::ops::{Deref, DerefMut}; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 9 | pub struct Uint(pub der::asn1::Uint); 10 | 11 | impl Deref for Uint { 12 | type Target = der::asn1::Uint; 13 | 14 | fn deref(&self) -> &Self::Target { 15 | &self.0 16 | } 17 | } 18 | 19 | impl DerefMut for Uint { 20 | fn deref_mut(&mut self) -> &mut Self::Target { 21 | &mut self.0 22 | } 23 | } 24 | 25 | impl From for Uint { 26 | fn from(value: der::asn1::Uint) -> Self { 27 | Self(value) 28 | } 29 | } 30 | 31 | impl From for der::asn1::Uint { 32 | fn from(value: Uint) -> Self { 33 | value.0 34 | } 35 | } 36 | 37 | impl Uint { 38 | pub fn as_inner(&self) -> &der::asn1::Uint { 39 | &self.0 40 | } 41 | 42 | pub fn as_inner_mut(&mut self) -> &mut der::asn1::Uint { 43 | &mut self.0 44 | } 45 | } 46 | 47 | impl Read for Uint { 48 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 49 | self.0.as_bytes().read(buf) 50 | } 51 | } 52 | 53 | impl TryFrom for u64 { 54 | type Error = crate::errors::InvalidInput; 55 | 56 | fn try_from(value: Uint) -> Result { 57 | if value.as_bytes().len() > 8 { 58 | return Err(crate::errors::InvalidInput::Malformed(format!( 59 | "SerialNumber holds {} bytes, but is only allowed to hold a maximum of 8 bytes (64 bit unsigned integer)", 60 | value.as_bytes().len() 61 | ))); 62 | } 63 | let mut buf = [0u8; 8]; 64 | let mut value = value.as_bytes().to_vec(); 65 | value.reverse(); 66 | if value.as_slice().read(&mut buf).is_err() { 67 | Ok(0) 68 | } else { 69 | buf.reverse(); 70 | Ok(u64::from_be_bytes(buf)) 71 | } 72 | } 73 | } 74 | 75 | impl From for Uint { 76 | fn from(value: u64) -> Self { 77 | Uint(der::asn1::Uint::new(value.to_be_bytes().as_slice()).unwrap()) 78 | } 79 | } 80 | 81 | #[test] 82 | fn uint_to_u64_and_vice_versa() { 83 | let u641 = 3761284836712u64; 84 | let uint = Uint::from(u641); 85 | let u642 = u64::try_from(uint).unwrap(); 86 | assert_eq!(u641, u642) 87 | } 88 | 89 | #[test] 90 | fn uint_to_u64_fails_on_too_big_uint() { 91 | let big_number = u128::MAX.to_be_bytes(); 92 | assert!(u64::try_from(Uint(der::asn1::Uint::new(&big_number).unwrap())).is_err()); 93 | let big_number = (u64::MAX as u128 + 1).to_be_bytes(); 94 | let uint = Uint(der::asn1::Uint::new(&big_number).unwrap()); 95 | assert!(u64::try_from(uint).is_err()); 96 | } 97 | -------------------------------------------------------------------------------- /src/types/der/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #[allow(missing_docs)] 6 | pub mod asn1; 7 | -------------------------------------------------------------------------------- /src/types/encrypted_pkm.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use der::asn1::BitString; 6 | 7 | use super::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfo}; 8 | use super::x509_cert::SerialNumber; 9 | 10 | #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] 11 | #[derive(Debug, Clone, PartialEq, Eq)] 12 | /// A private key material structure for storing encrypted private key material on a home server. 13 | /// 14 | /// For more information, such as how this type is represented in JSON, see the type definition of 15 | /// `EncryptedPKM` on the [polyproto documentation website](https://docs.polyphony.chat/APIs/core/Types/encrypted_pkm/) 16 | pub struct EncryptedPkm { 17 | /// The serial number of the certificate that this private key material is associated with. 18 | pub serial_number: SerialNumber, 19 | /// The encrypted private key material, along with the signature algorithm of the private key. 20 | pub key_data: PrivateKeyInfo, 21 | /// The encryption algorithm used to encrypt the private key material. 22 | pub encryption_algorithm: AlgorithmIdentifierOwned, 23 | } 24 | 25 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 26 | /// Private key material with additional information about the private keys' algorithm. 27 | pub struct PrivateKeyInfo { 28 | /// The algorithm of the private key. 29 | pub algorithm: AlgorithmIdentifierOwned, 30 | /// The encrypted private key material. 31 | pub encrypted_private_key_bitstring: BitString, 32 | } 33 | 34 | impl From for PrivateKeyInfo { 35 | fn from(value: SubjectPublicKeyInfo) -> Self { 36 | PrivateKeyInfo { 37 | algorithm: value.algorithm.clone().into(), 38 | encrypted_private_key_bitstring: value.subject_public_key.clone(), 39 | } 40 | } 41 | } 42 | 43 | impl From for SubjectPublicKeyInfo { 44 | fn from(value: PrivateKeyInfo) -> Self { 45 | spki::SubjectPublicKeyInfoOwned { 46 | algorithm: value.algorithm.into(), 47 | subject_public_key: value.encrypted_private_key_bitstring, 48 | } 49 | .into() 50 | } 51 | } 52 | 53 | #[cfg(feature = "serde")] 54 | mod serde_support { 55 | use der::pem::LineEnding; 56 | use serde::de::Visitor; 57 | use serde::{Deserialize, Serialize}; 58 | 59 | use crate::types::spki::SubjectPublicKeyInfo; 60 | 61 | use super::PrivateKeyInfo; 62 | 63 | struct PrivateKeyInfoVisitor; 64 | 65 | impl Visitor<'_> for PrivateKeyInfoVisitor { 66 | type Value = PrivateKeyInfo; 67 | 68 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 69 | 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") 70 | } 71 | 72 | fn visit_str(self, v: &str) -> Result 73 | where 74 | E: serde::de::Error, 75 | { 76 | SubjectPublicKeyInfo::from_pem(v.as_bytes()) 77 | .map_err(serde::de::Error::custom) 78 | .map(Into::into) 79 | } 80 | } 81 | 82 | impl<'de> Deserialize<'de> for crate::types::encrypted_pkm::PrivateKeyInfo { 83 | fn deserialize(deserializer: D) -> Result 84 | where 85 | D: serde::Deserializer<'de>, 86 | { 87 | deserializer.deserialize_str(PrivateKeyInfoVisitor) 88 | } 89 | } 90 | 91 | impl Serialize for crate::types::encrypted_pkm::PrivateKeyInfo { 92 | fn serialize(&self, serializer: S) -> Result 93 | where 94 | S: serde::Serializer, 95 | { 96 | serializer.serialize_str( 97 | SubjectPublicKeyInfo::from(self.clone()) 98 | .to_pem(LineEnding::LF) 99 | .map_err(serde::ser::Error::custom)? 100 | .as_str(), 101 | ) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/types/federation_id.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use regex::Regex; 6 | 7 | use crate::Constrained; 8 | use crate::errors::{ConstraintError, ERR_MSG_FEDERATION_ID_REGEX}; 9 | 10 | /// The regular expression for a valid `FederationId`. 11 | pub static REGEX_FEDERATION_ID: &str = r"\b([a-z0-9._%+-]+)@([a-z0-9-]+(\.[a-z0-9-]+)*)$"; 12 | /// The regular expression for a valid domain name. 13 | pub static REGEX_DOMAIN_NAME: &str = r"\b([a-z0-9-]+(\.[a-z0-9-]+)*)$"; 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 16 | /// Common types of federation identifiers. 17 | pub enum Identifer { 18 | /// A "domain name", identifying an instance 19 | Instance(DomainName), 20 | /// A "federation ID", identifying a unique actor 21 | FederationId(FederationId), 22 | } 23 | 24 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 25 | /// Domain names are what identify an instance. 26 | pub struct DomainName { 27 | pub(crate) value: String, 28 | } 29 | 30 | impl DomainName { 31 | /// Validates input, then creates a new [DomainName]. 32 | pub fn new(domain_name: &str) -> Result { 33 | let regex = Regex::new(REGEX_DOMAIN_NAME).unwrap(); 34 | if regex.is_match(domain_name) { 35 | Ok(Self { 36 | value: domain_name.to_string(), 37 | }) 38 | } else { 39 | Err(ConstraintError::Malformed(Some(String::from( 40 | "Supplied domain name does not match regex", 41 | )))) 42 | } 43 | } 44 | } 45 | 46 | impl std::fmt::Display for DomainName { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | write!(f, "{}", self.value) 49 | } 50 | } 51 | 52 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 53 | /// A `FederationId` is a globally unique identifier for an actor in the context of polyproto. 54 | pub struct FederationId { 55 | /// Must be unique on each instance. 56 | pub(crate) local_name: String, 57 | /// Includes top-level domain, second-level domain and other subdomains. Address which the actors' home server can be reached at. 58 | pub(crate) domain_name: String, 59 | } 60 | 61 | impl FederationId { 62 | /// Validates input, then creates a new `FederationId`. 63 | pub fn new(id: &str) -> Result { 64 | let regex = Regex::new(REGEX_FEDERATION_ID).unwrap(); 65 | let matches = { 66 | let mut x = String::new(); 67 | regex 68 | .find_iter(id) 69 | .map(|y| y.as_str()) 70 | .for_each(|y| x.push_str(y)); 71 | x 72 | }; 73 | if regex.is_match(&matches) { 74 | let separator_position = id.find('@').unwrap(); 75 | let local_name = id[0..separator_position].to_string(); 76 | let domain_name = id[separator_position + 1..].to_string(); 77 | let fid = Self { 78 | local_name, 79 | domain_name, 80 | }; 81 | fid.validate(None)?; 82 | Ok(fid) 83 | } else { 84 | Err(ConstraintError::Malformed(Some( 85 | ERR_MSG_FEDERATION_ID_REGEX.to_string(), 86 | ))) 87 | } 88 | } 89 | } 90 | 91 | impl TryFrom<&str> for FederationId { 92 | type Error = ConstraintError; 93 | 94 | fn try_from(value: &str) -> Result { 95 | FederationId::new(value) 96 | } 97 | } 98 | 99 | impl std::fmt::Display for FederationId { 100 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 101 | write!(f, "{}@{}", self.local_name, self.domain_name) 102 | } 103 | } 104 | 105 | #[cfg(feature = "serde")] 106 | mod serde { 107 | use serde::de::Visitor; 108 | use serde::{Deserialize, Serialize}; 109 | 110 | use crate::errors::{ERR_MSG_DOMAIN_NAME_REGEX, ERR_MSG_FEDERATION_ID_REGEX}; 111 | 112 | use super::{DomainName, FederationId, Identifer}; 113 | 114 | struct FidVisitor; 115 | 116 | impl Visitor<'_> for FidVisitor { 117 | type Value = FederationId; 118 | 119 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 120 | formatter.write_str("a valid polyproto federation ID") 121 | } 122 | 123 | fn visit_str(self, v: &str) -> Result 124 | where 125 | E: serde::de::Error, 126 | { 127 | FederationId::new(v).map_err(|_| E::custom(ERR_MSG_FEDERATION_ID_REGEX.to_string())) 128 | } 129 | } 130 | 131 | impl<'de> Deserialize<'de> for FederationId { 132 | fn deserialize(deserializer: D) -> Result 133 | where 134 | D: serde::Deserializer<'de>, 135 | { 136 | deserializer.deserialize_str(FidVisitor) 137 | } 138 | } 139 | 140 | impl Serialize for FederationId { 141 | fn serialize(&self, serializer: S) -> Result 142 | where 143 | S: serde::Serializer, 144 | { 145 | serializer.serialize_str(&self.to_string()) 146 | } 147 | } 148 | 149 | struct DnVisitor; 150 | 151 | impl Visitor<'_> for DnVisitor { 152 | type Value = DomainName; 153 | 154 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 155 | formatter.write_str("a valid domain name (please open a bug report if your domain name is valid and still caused this error)") 156 | } 157 | 158 | fn visit_str(self, v: &str) -> Result 159 | where 160 | E: serde::de::Error, 161 | { 162 | DomainName::new(v).map_err(|_| E::custom(ERR_MSG_DOMAIN_NAME_REGEX.to_string())) 163 | } 164 | } 165 | 166 | impl<'de> Deserialize<'de> for DomainName { 167 | fn deserialize(deserializer: D) -> Result 168 | where 169 | D: serde::Deserializer<'de>, 170 | { 171 | deserializer.deserialize_str(DnVisitor) 172 | } 173 | } 174 | 175 | impl Serialize for DomainName { 176 | fn serialize(&self, serializer: S) -> Result 177 | where 178 | S: serde::Serializer, 179 | { 180 | serializer.serialize_str(&self.to_string()) 181 | } 182 | } 183 | 184 | struct IdVisitor; 185 | 186 | impl Visitor<'_> for IdVisitor { 187 | type Value = Identifer; 188 | 189 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 190 | formatter.write_str("a valid DomainName or FederationId") 191 | } 192 | 193 | fn visit_str(self, v: &str) -> Result 194 | where 195 | E: serde::de::Error, 196 | { 197 | if let Ok(fid) = FederationId::new(v) { 198 | Ok(Identifer::FederationId(fid) as Self::Value) 199 | } else if let Ok(dn) = DomainName::new(v) { 200 | Ok(Identifer::Instance(dn) as Self::Value) 201 | } else { 202 | Err(E::custom( 203 | "passed string is neither a valid DomainName nor a valid FederationId", 204 | )) 205 | } 206 | } 207 | } 208 | 209 | impl<'de> Deserialize<'de> for Identifer { 210 | fn deserialize(deserializer: D) -> Result 211 | where 212 | D: serde::Deserializer<'de>, 213 | { 214 | deserializer.deserialize_str(IdVisitor) 215 | } 216 | } 217 | 218 | impl Serialize for Identifer { 219 | fn serialize(&self, serializer: S) -> Result 220 | where 221 | S: serde::Serializer, 222 | { 223 | match self { 224 | Identifer::Instance(domain_name) => domain_name.serialize(serializer), 225 | Identifer::FederationId(federation_id) => federation_id.serialize(serializer), 226 | } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/types/keytrial.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::certs::idcert::IdCert; 6 | use crate::errors::PublicKeyError; 7 | use crate::key::{PrivateKey, PublicKey}; 8 | use crate::signature::Signature; 9 | 10 | use super::der::asn1::Uint; 11 | 12 | #[cfg_attr(feature = "serde", serde_with::serde_as)] 13 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 14 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 15 | /// A completed key trial, as an actor would send to the server. 16 | #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] 17 | pub struct KeyTrialResponse { 18 | /// The signature produced by signing the key trial string using a private identity key. 19 | pub signature: String, 20 | #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] 21 | /// The serial number of the ID-Cert corresponding to the private identity key used to sign the key trial string. 22 | pub serial_number: u64, 23 | } 24 | 25 | #[cfg_attr(feature = "serde", serde_with::serde_as)] 26 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 27 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 28 | /// A key trial as sent from the server to an actor. 29 | /// Used to verify an actor's private identity key possession, without revealing the private key itself 30 | #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] 31 | pub struct KeyTrial { 32 | /// The key trial, which the client should sign with their private identity key. 33 | pub trial: String, 34 | #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] 35 | /// The UNIX timestamp after which the key trial expires. 36 | pub expires: u64, 37 | } 38 | 39 | impl KeyTrialResponse { 40 | /// Try to convert `self.signature` to a signature `S where S: polyproto::signature::Signature` 41 | pub fn signature_as_signature(&self) -> Result { 42 | S::try_from_hex(&self.signature).map_err(|_| PublicKeyError::BadSignature) 43 | } 44 | 45 | /// Verify that a [KeyTrialResponse] is valid for a given actor public key and a given [KeyTrial]. 46 | /// 47 | /// ## Parameters 48 | /// 49 | /// - `verifying_timestamp_unix`: The timestamp at which the [KeyTrialResponse] was received to ensure, 50 | /// that the response was generated in the timeframe dictated by `(KeyTrial as self).expires`. 51 | pub fn verify_response>( 52 | &self, 53 | key_trial: &KeyTrial, 54 | verifying_timestamp_unix: u64, 55 | actor_public_key: &P, 56 | ) -> Result<(), PublicKeyError> { 57 | let signature = self.signature_as_signature::()?; 58 | if key_trial.expires > verifying_timestamp_unix { 59 | Err(PublicKeyError::BadSignature) 60 | } else { 61 | actor_public_key.verify_signature(&signature, key_trial.trial.as_bytes()) 62 | } 63 | } 64 | } 65 | 66 | impl KeyTrial { 67 | /// With a provided certificate and the matching `signing_key`, generate a [KeyTrialResponse] from 68 | /// a [KeyTrial]. 69 | /// 70 | /// Does not check if the provided `signing_key` corresponds to the public key in the [IdCert]. 71 | pub fn into_response, S: Signature>( 72 | self, 73 | signing_key: T, 74 | cert: &IdCert, 75 | ) -> Result 76 | where 77 | T::PublicKey: PublicKey, 78 | { 79 | let signature = signing_key.sign(self.trial.as_bytes()); 80 | Ok(KeyTrialResponse { 81 | signature: signature.as_hex(), 82 | serial_number: u64::try_from(Uint(cert.id_cert_tbs.serial_number.clone())) 83 | .map_err(|_| PublicKeyError::BadPublicKeyInfo)?, 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/types/p2_export.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::collections::HashMap; 6 | use std::ffi::OsString; 7 | 8 | use super::ResourceAccessProperties; 9 | 10 | /// A P2Export data export. 11 | #[derive(Debug, Clone, PartialEq, Eq)] 12 | pub struct P2Export { 13 | /// Generic messages 14 | messages: MessageBatches, 15 | /// File name 16 | name: OsString, 17 | } 18 | 19 | #[derive(Debug, Clone, PartialEq, Eq)] 20 | pub struct MessageBatches { 21 | rawr: Option>, 22 | } 23 | 24 | #[derive(Debug, Clone, PartialEq, Eq)] 25 | pub struct RawrContent { 26 | resources: HashMap, 27 | access_properties: HashMap, 28 | } 29 | 30 | #[cfg(feature = "reqwest")] 31 | mod reqwest { 32 | use crate::errors::InvalidInput; 33 | 34 | use super::P2Export; 35 | 36 | impl TryFrom> for reqwest::multipart::Form { 37 | type Error = InvalidInput; 38 | // TODO 39 | fn try_from(value: P2Export) -> Result { 40 | todo!() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/types/rawr.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use super::Identifer; 6 | 7 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 8 | /// Standard HTTP file. 9 | pub struct Resource { 10 | /// MIME content type of this resource 11 | pub content_type: Option, 12 | /// File name of this resource 13 | pub filename: Option, 14 | /// Contents, as raw bytes 15 | pub contents: Vec, 16 | } 17 | 18 | #[cfg(feature = "reqwest")] 19 | impl Resource { 20 | /// Tries to convert [Self] into a [::reqwest::multipart::Form]. Conversion will fail, 21 | /// if `content_type` is present and cannot be parsed as [mime::Mime] by `reqwest` 22 | pub fn into_multipart(self) -> Result<::reqwest::multipart::Form, crate::errors::InvalidInput> { 23 | use ::reqwest::multipart::Form; 24 | Form::try_from(self) 25 | } 26 | } 27 | 28 | #[cfg_attr(feature = "serde", serde_with::serde_as)] 29 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 30 | #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] 31 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 32 | /// Information about a [Resource], including size, the resource ID and a resources' [ResourceAccessProperties] 33 | pub struct ResourceInformation { 34 | /// The unique identifier of this resource 35 | pub resource_id: String, 36 | #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] 37 | /// Size of this resource, in bytes 38 | pub size: u64, 39 | /// Access properties of this resource. 40 | pub access: ResourceAccessProperties, 41 | } 42 | 43 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 44 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 45 | /// `ResourceAccessProperties` define which actors and instances may access an uploaded resource. 46 | pub struct ResourceAccessProperties { 47 | /// Whether the resource should be private by default. Private resources can only be accessed by the uploader and by instances and actors declared in the `allowlist`. 48 | pub private: bool, 49 | /// Whether the resource should be publicly retrievable, i.e. without requiring authentication. If this is `true`, the allow- and denylists are ignored. 50 | pub public: bool, 51 | /// A list of actors and/or instances allowed to access this resource. 52 | pub allowlist: Vec, 53 | /// A list of actors and/or instances who cannot have access to this resource. 54 | pub denylist: Vec, 55 | } 56 | 57 | #[cfg(feature = "reqwest")] 58 | mod reqwest { 59 | use reqwest::multipart::{Form, Part}; 60 | 61 | use crate::errors::InvalidInput; 62 | 63 | use super::Resource; 64 | 65 | impl TryFrom for Form { 66 | type Error = InvalidInput; 67 | /// Performs the conversion. Conversion will fail, if `content_type` is present and cannot 68 | /// be parsed as [mime::Mime] by `reqwest` 69 | fn try_from(resource: Resource) -> Result { 70 | let mut form = Form::new(); 71 | 72 | let mut part = Part::bytes(resource.contents); 73 | 74 | if let Some(content_type) = resource.content_type { 75 | part = part 76 | .mime_str(&content_type) 77 | .map_err(|e| InvalidInput::Malformed(e.to_string()))?; 78 | } 79 | 80 | if let Some(filename) = resource.filename { 81 | part = part.file_name(filename); 82 | } 83 | 84 | form = form.part("file", part); 85 | 86 | Ok(form) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/types/service.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #[cfg(feature = "reqwest")] 6 | type Url = url::Url; 7 | #[cfg(not(feature = "reqwest"))] 8 | type Url = String; 9 | 10 | use crate::Constrained; 11 | use crate::errors::ConstraintError; 12 | 13 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 14 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 15 | /// A resource representing information about a discoverable service for an actor. You can learn more about 16 | /// services and discoverability by reading [section #9](https://docs.polyphony.chat/Protocol%20Specifications/core#9-services) of 17 | /// the core protocol specification. 18 | /// 19 | /// This resource contains information about the name of the service that is being made discoverable, 20 | /// the URL of the service provider, and whether this service provider is the primary service provider 21 | /// for the actor. 22 | /// 23 | /// For more information, see the [type definition in the core protocol API documentation](https://docs.polyphony.chat/APIs/core/Types/service/) 24 | pub struct Service { 25 | /// The name of the service. 26 | pub service: ServiceName, 27 | /// The base URL of the service provider, not including `/.p2/`. Trailing slashes 28 | /// are allowed. If `(/).p2/` is added to the URL specified here, a polyproto 29 | /// client should be able to access the HTTP API routes provided by the service. 30 | pub url: Url, 31 | /// Whether the service provider specified in the `url` field is the primary service provider 32 | /// for this service and actor. 33 | pub primary: bool, 34 | } 35 | 36 | impl Service { 37 | /// Create a new [Service] resource. 38 | pub fn new(service_name: &str, url: Url, primary: bool) -> Result { 39 | let service_name = ServiceName::new(service_name)?; 40 | Ok(Self { 41 | service: service_name, 42 | url, 43 | primary, 44 | }) 45 | } 46 | } 47 | 48 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 49 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 50 | /// A valid service name, formatted according to 51 | /// [section #8.2: Namespaces](https://docs.polyphony.chat/Protocol%20Specifications/core#82-namespaces) 52 | /// in the core protocol specification. 53 | pub struct ServiceName { 54 | inner: String, 55 | } 56 | 57 | impl std::fmt::Display for ServiceName { 58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 | write!(f, "{}", self.inner) 60 | } 61 | } 62 | 63 | impl ServiceName { 64 | /// Create a new [ServiceName] from a string slice. 65 | pub fn new(name: &str) -> Result { 66 | let service_name = Self { 67 | inner: name.to_string(), 68 | }; 69 | service_name.validate(None)?; 70 | Ok(service_name) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/types/spki/algorithmidentifierowned.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::ops::{Deref, DerefMut}; 6 | 7 | use der::{Any, Decode, Encode}; 8 | use spki::ObjectIdentifier; 9 | 10 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] 11 | /// `AlgorithmIdentifier` reference which has `Any` parameters. 12 | /// 13 | /// A wrapper around `spki::AlgorithmIdentifierOwned`, which provides `serde` support, if enabled by 14 | /// the `serde` feature. 15 | /// 16 | /// ## De-/Serialization expectations 17 | /// 18 | /// This type expects a DER encoded AlgorithmIdentifier with optional der::Any parameters. The DER 19 | /// encoded data has to be provided in the form of an array of bytes. Types that fulfill this 20 | /// expectation are, for example, `&[u8]`, `Vec` and `&[u8; N]`. 21 | pub struct AlgorithmIdentifierOwned(spki::AlgorithmIdentifierOwned); 22 | 23 | impl AlgorithmIdentifierOwned { 24 | /// Create a new `AlgorithmIdentifierOwned`. 25 | pub fn new(oid: ObjectIdentifier, parameters: Option) -> Self { 26 | Self(spki::AlgorithmIdentifierOwned { oid, parameters }) 27 | } 28 | 29 | /// Try to encode this type as DER. 30 | pub fn to_der(&self) -> Result, der::Error> { 31 | self.0.to_der() 32 | } 33 | 34 | /// Try to decode this type from DER. 35 | pub fn from_der(bytes: &[u8]) -> Result { 36 | spki::AlgorithmIdentifierOwned::from_der(bytes).map(Self) 37 | } 38 | } 39 | 40 | impl Deref for AlgorithmIdentifierOwned { 41 | type Target = spki::AlgorithmIdentifierOwned; 42 | 43 | fn deref(&self) -> &Self::Target { 44 | &self.0 45 | } 46 | } 47 | 48 | impl DerefMut for AlgorithmIdentifierOwned { 49 | fn deref_mut(&mut self) -> &mut Self::Target { 50 | &mut self.0 51 | } 52 | } 53 | 54 | impl From for AlgorithmIdentifierOwned { 55 | fn from(value: spki::AlgorithmIdentifierOwned) -> Self { 56 | Self(value) 57 | } 58 | } 59 | 60 | impl From for spki::AlgorithmIdentifierOwned { 61 | fn from(value: AlgorithmIdentifierOwned) -> Self { 62 | value.0 63 | } 64 | } 65 | 66 | #[cfg(feature = "serde")] 67 | mod serde_support { 68 | use super::AlgorithmIdentifierOwned; 69 | use serde::de::Visitor; 70 | use serde::{Deserialize, Serialize}; 71 | struct AlgorithmIdentifierVisitor; 72 | 73 | impl<'de> Visitor<'de> for AlgorithmIdentifierVisitor { 74 | type Value = AlgorithmIdentifierOwned; 75 | 76 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 77 | formatter 78 | .write_str("a valid DER encoded byte slice representing an AlgorithmIdentifier") 79 | } 80 | 81 | fn visit_bytes(self, v: &[u8]) -> Result 82 | where 83 | E: serde::de::Error, 84 | { 85 | AlgorithmIdentifierOwned::from_der(v).map_err(serde::de::Error::custom) 86 | } 87 | 88 | fn visit_seq(self, mut seq: A) -> Result 89 | where 90 | A: serde::de::SeqAccess<'de>, 91 | { 92 | let mut bytes: Vec = Vec::new(); // Create a new Vec to store the bytes 93 | while let Some(byte) = seq.next_element()? { 94 | // "Iterate" over the sequence, assuming each element is a byte 95 | bytes.push(byte) // Push the byte to the Vec 96 | } 97 | AlgorithmIdentifierOwned::from_der(&bytes).map_err(serde::de::Error::custom) 98 | } 99 | } 100 | 101 | impl<'de> Deserialize<'de> for AlgorithmIdentifierOwned { 102 | fn deserialize(deserializer: D) -> Result 103 | where 104 | D: serde::Deserializer<'de>, 105 | { 106 | deserializer.deserialize_bytes(AlgorithmIdentifierVisitor) 107 | } 108 | } 109 | 110 | impl Serialize for AlgorithmIdentifierOwned { 111 | fn serialize(&self, serializer: S) -> Result 112 | where 113 | S: serde::Serializer, 114 | { 115 | let der = self.to_der().map_err(serde::ser::Error::custom)?; 116 | serializer.serialize_bytes(&der) 117 | } 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod test { 123 | use std::str::FromStr; 124 | 125 | use der::asn1::BitString; 126 | use der::{Any, Decode, Encode}; 127 | use log::trace; 128 | use serde_json::json; 129 | use spki::ObjectIdentifier; 130 | 131 | use crate::testing_utils::init_logger; 132 | 133 | use super::AlgorithmIdentifierOwned; 134 | 135 | #[test] 136 | fn de_serialize() { 137 | init_logger(); 138 | let oid = ObjectIdentifier::from_str("1.1.1.4.5").unwrap(); 139 | let alg = AlgorithmIdentifierOwned::new(oid, None); 140 | let json = json!(alg); 141 | let deserialized: AlgorithmIdentifierOwned = serde_json::from_value(json).unwrap(); 142 | assert_eq!(alg, deserialized); 143 | trace!("deserialized: {:?}", deserialized); 144 | trace!("original: {:?}", alg); 145 | 146 | let bytes = [48, 6, 6, 3, 43, 6, 1, 5, 1, 4, 5, 5, 23, 2, 0, 0]; 147 | let bitstring = BitString::from_bytes(&bytes).unwrap(); 148 | let alg = AlgorithmIdentifierOwned::new( 149 | oid, 150 | Some(Any::from_der(&bitstring.to_der().unwrap()).unwrap()), 151 | ); 152 | let json = json!(alg); 153 | let deserialized: AlgorithmIdentifierOwned = serde_json::from_value(json).unwrap(); 154 | trace!("deserialized: {:?}", deserialized); 155 | trace!("original: {:?}", alg); 156 | assert_eq!(alg, deserialized); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/types/spki/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #![allow(missing_docs)] 6 | 7 | pub mod algorithmidentifierowned; 8 | pub mod subjectpublickeyinfo; 9 | 10 | pub use algorithmidentifierowned::*; 11 | pub use subjectpublickeyinfo::*; 12 | -------------------------------------------------------------------------------- /src/types/spki/subjectpublickeyinfo.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::ops::{Deref, DerefMut}; 6 | 7 | use super::super::spki::AlgorithmIdentifierOwned; 8 | use der::asn1::BitString; 9 | use der::pem::LineEnding; 10 | use der::{Decode, DecodePem, Encode, EncodePem}; 11 | 12 | #[derive(Clone, Debug, PartialEq, Eq)] 13 | pub struct SubjectPublicKeyInfo(spki::SubjectPublicKeyInfoOwned); 14 | 15 | impl SubjectPublicKeyInfo { 16 | pub fn new(algorithm: AlgorithmIdentifierOwned, subject_public_key: BitString) -> Self { 17 | Self(spki::SubjectPublicKeyInfoOwned { 18 | algorithm: algorithm.into(), 19 | subject_public_key, 20 | }) 21 | } 22 | 23 | /// Try to decode this type from PEM. 24 | pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { 25 | spki::SubjectPublicKeyInfo::from_pem(pem).map(Self) 26 | } 27 | /// Try to decode this type from DER. 28 | pub fn from_der(value: &[u8]) -> Result { 29 | spki::SubjectPublicKeyInfo::from_der(value).map(Self) 30 | } 31 | 32 | /// Try to encode this type as PEM. 33 | pub fn to_pem(&self, line_ending: LineEnding) -> Result { 34 | self.0.to_pem(line_ending) 35 | } 36 | 37 | /// Try to encode this type as DER. 38 | pub fn to_der(&self) -> Result, der::Error> { 39 | self.0.to_der() 40 | } 41 | } 42 | 43 | impl From for SubjectPublicKeyInfo { 44 | fn from(spki: spki::SubjectPublicKeyInfoOwned) -> Self { 45 | Self(spki) 46 | } 47 | } 48 | 49 | impl From for spki::SubjectPublicKeyInfoOwned { 50 | fn from(spki: SubjectPublicKeyInfo) -> Self { 51 | spki.0 52 | } 53 | } 54 | 55 | impl Deref for SubjectPublicKeyInfo { 56 | type Target = spki::SubjectPublicKeyInfoOwned; 57 | 58 | fn deref(&self) -> &Self::Target { 59 | &self.0 60 | } 61 | } 62 | 63 | impl DerefMut for SubjectPublicKeyInfo { 64 | fn deref_mut(&mut self) -> &mut Self::Target { 65 | &mut self.0 66 | } 67 | } 68 | 69 | #[cfg(feature = "serde")] 70 | mod serde_support { 71 | use der::pem::LineEnding; 72 | use serde::de::Visitor; 73 | use serde::{Deserialize, Serialize}; 74 | 75 | use super::SubjectPublicKeyInfo; 76 | struct SubjectPublicKeyInfoVisitor; 77 | 78 | impl Visitor<'_> for SubjectPublicKeyInfoVisitor { 79 | type Value = SubjectPublicKeyInfo; 80 | 81 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 82 | formatter.write_str("a valid, PEM or DER encoded SubjectPublicKeyInfo") 83 | } 84 | 85 | fn visit_str(self, v: &str) -> Result 86 | where 87 | E: serde::de::Error, 88 | { 89 | SubjectPublicKeyInfo::from_pem(v).map_err(serde::de::Error::custom) 90 | } 91 | 92 | fn visit_bytes(self, v: &[u8]) -> Result 93 | where 94 | E: serde::de::Error, 95 | { 96 | SubjectPublicKeyInfo::from_der(v).map_err(serde::de::Error::custom) 97 | } 98 | } 99 | 100 | impl<'de> Deserialize<'de> for SubjectPublicKeyInfo { 101 | fn deserialize(deserializer: D) -> Result 102 | where 103 | D: serde::Deserializer<'de>, 104 | { 105 | deserializer.deserialize_any(SubjectPublicKeyInfoVisitor) 106 | } 107 | } 108 | 109 | impl Serialize for SubjectPublicKeyInfo { 110 | fn serialize(&self, serializer: S) -> Result 111 | where 112 | S: serde::Serializer, 113 | { 114 | let pem = self 115 | .to_pem(LineEnding::default()) 116 | .map_err(serde::ser::Error::custom)?; 117 | serializer.serialize_str(&pem) 118 | } 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod test { 124 | use std::str::FromStr; 125 | 126 | use der::asn1::BitString; 127 | use serde_json::json; 128 | use spki::ObjectIdentifier; 129 | 130 | use crate::types::spki::AlgorithmIdentifierOwned; 131 | 132 | use super::SubjectPublicKeyInfo; 133 | 134 | #[test] 135 | fn deserialize_serialize_spki_json() { 136 | let oids = [ 137 | ObjectIdentifier::from_str("1.1.3.1").unwrap(), 138 | ObjectIdentifier::from_str("2.23.5672.1").unwrap(), 139 | ObjectIdentifier::from_str("0.3.1.1").unwrap(), 140 | ObjectIdentifier::from_str("1.2.3.4.5.6.7.8.9.0.12.3.4.5.6.67").unwrap(), 141 | ObjectIdentifier::from_str("1.2.1122").unwrap(), 142 | ]; 143 | 144 | for oid in oids.into_iter() { 145 | let spki = SubjectPublicKeyInfo::new( 146 | AlgorithmIdentifierOwned::new(oid, None), 147 | BitString::from_bytes(&[0x00, 0x01, 0x02]).unwrap(), 148 | ); 149 | let spki_json = json!(&spki); 150 | let spki2: SubjectPublicKeyInfo = serde_json::from_value(spki_json.clone()).unwrap(); 151 | assert_eq!(spki, spki2); 152 | } 153 | } 154 | 155 | #[test] 156 | fn deserialize_serialize_spki_pem() { 157 | let oids = [ 158 | ObjectIdentifier::from_str("1.1.3.1").unwrap(), 159 | ObjectIdentifier::from_str("2.23.5672.1").unwrap(), 160 | ObjectIdentifier::from_str("0.3.1.1").unwrap(), 161 | ObjectIdentifier::from_str("1.2.3.4.5.6.7.8.9.0.12.3.4.5.6.67").unwrap(), 162 | ObjectIdentifier::from_str("1.2.1122").unwrap(), 163 | ]; 164 | 165 | for oid in oids.into_iter() { 166 | let spki = SubjectPublicKeyInfo::new( 167 | AlgorithmIdentifierOwned::new(oid, None), 168 | BitString::from_bytes(&[0x00, 0x01, 0x02]).unwrap(), 169 | ); 170 | let spki_pem = spki.to_pem(der::pem::LineEnding::LF).unwrap(); 171 | let spki2 = SubjectPublicKeyInfo::from_pem(spki_pem).unwrap(); 172 | assert_eq!(spki, spki2); 173 | } 174 | } 175 | 176 | #[test] 177 | fn deserialize_serialize_spki_der() { 178 | let oids = [ 179 | ObjectIdentifier::from_str("1.1.3.1").unwrap(), 180 | ObjectIdentifier::from_str("2.23.5672.1").unwrap(), 181 | ObjectIdentifier::from_str("0.3.1.1").unwrap(), 182 | ObjectIdentifier::from_str("1.2.3.4.5.6.7.8.9.0.12.3.4.5.6.67").unwrap(), 183 | ObjectIdentifier::from_str("1.2.1122").unwrap(), 184 | ]; 185 | 186 | for oid in oids.into_iter() { 187 | let spki = SubjectPublicKeyInfo::new( 188 | AlgorithmIdentifierOwned::new(oid, None), 189 | BitString::from_bytes(&[0x00, 0x01, 0x02]).unwrap(), 190 | ); 191 | let spki_der = spki.to_der().unwrap(); 192 | let spki2 = SubjectPublicKeyInfo::from_der(&spki_der).unwrap(); 193 | assert_eq!(spki, spki2); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/types/x509_cert/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #![allow(missing_docs)] 6 | 7 | pub mod serialnumber; 8 | 9 | pub use serialnumber::*; 10 | -------------------------------------------------------------------------------- /src/types/x509_cert/serialnumber.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::ops::{Deref, DerefMut}; 6 | 7 | use log::trace; 8 | 9 | use crate::errors::{CertificateConversionError, InvalidInput}; 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq)] 12 | /// Wrapper type around [x509_cert::serial_number::SerialNumber], providing serde support, if the 13 | /// `serde` feature is enabled. See "De-/serialization value expectations" below for more 14 | /// information. 15 | /// 16 | /// [RFC 5280 Section 4.1.2.2.] Serial Number 17 | /// 18 | /// The serial number MUST be a positive integer assigned by the CA to 19 | /// each certificate. It MUST be unique for each certificate issued by a 20 | /// given CA (i.e., the issuer name and serial number identify a unique 21 | /// certificate). CAs MUST force the serialNumber to be a non-negative 22 | /// integer. 23 | /// 24 | /// Given the uniqueness requirements above, serial numbers can be 25 | /// expected to contain long integers. Certificate users MUST be able to 26 | /// handle serialNumber values up to 20 octets. Conforming CAs MUST NOT 27 | /// use serialNumber values longer than 20 octets. 28 | /// 29 | /// Note: Non-conforming CAs may issue certificates with serial numbers 30 | /// that are negative or zero. Certificate users SHOULD be prepared to 31 | /// gracefully handle such certificates. 32 | /// 33 | /// ## De-/serialization value expectations 34 | /// 35 | /// The serde de-/serialization implementation for [`SerialNumber`] expects a byte slice representing 36 | /// a positive integer. 37 | pub struct SerialNumber(::x509_cert::serial_number::SerialNumber); 38 | 39 | impl From<::x509_cert::serial_number::SerialNumber> for SerialNumber { 40 | fn from(inner: ::x509_cert::serial_number::SerialNumber) -> Self { 41 | SerialNumber(inner) 42 | } 43 | } 44 | 45 | impl From for ::x509_cert::serial_number::SerialNumber { 46 | fn from(value: SerialNumber) -> Self { 47 | value.0 48 | } 49 | } 50 | 51 | impl Deref for SerialNumber { 52 | type Target = ::x509_cert::serial_number::SerialNumber; 53 | 54 | fn deref(&self) -> &Self::Target { 55 | &self.0 56 | } 57 | } 58 | 59 | impl DerefMut for SerialNumber { 60 | fn deref_mut(&mut self) -> &mut Self::Target { 61 | &mut self.0 62 | } 63 | } 64 | 65 | impl SerialNumber { 66 | /// Create a new [`SerialNumber`] from a byte slice. 67 | /// 68 | /// The byte slice **must** represent a positive integer. 69 | pub fn new(bytes: &[u8]) -> Result { 70 | x509_cert::serial_number::SerialNumber::new(bytes).map(Into::into) 71 | } 72 | 73 | /// Borrow the inner byte slice which contains the least significant bytes 74 | /// of a big endian integer value with all leading zeros stripped. 75 | pub fn as_bytes(&self) -> &[u8] { 76 | self.0.as_bytes() 77 | } 78 | 79 | /// Try to convert the inner byte slice to a [u128]. 80 | /// 81 | /// Returns an error if the byte slice is empty, 82 | /// or if the byte slice is longer than 16 bytes. Leading zeros of byte slices are stripped, so 83 | /// 17 bytes are allowed, if the first byte is zero. 84 | pub fn try_as_u128(&self) -> Result { 85 | let mut bytes = self.as_bytes().to_vec(); 86 | if bytes.is_empty() { 87 | return Err(InvalidInput::Length { 88 | min_length: 1, 89 | max_length: 16, 90 | actual_length: 1.to_string(), 91 | } 92 | .into()); 93 | } 94 | if *bytes.first().unwrap() == 0 { 95 | bytes.remove(0); 96 | } 97 | trace!("bytes: {:?}", bytes); 98 | if bytes.len() > 16 { 99 | return Err(InvalidInput::Length { 100 | min_length: 1, 101 | max_length: 16, 102 | actual_length: bytes.len().to_string(), 103 | } 104 | .into()); 105 | } 106 | let mut buf = [0u8; 16]; 107 | buf[16 - bytes.len()..].copy_from_slice(&bytes); 108 | Ok(u128::from_be_bytes(buf)) 109 | } 110 | } 111 | 112 | impl TryFrom for u128 { 113 | type Error = CertificateConversionError; 114 | 115 | fn try_from(value: SerialNumber) -> Result { 116 | value.try_as_u128() 117 | } 118 | } 119 | 120 | impl From for SerialNumber { 121 | fn from(value: u128) -> Self { 122 | // All u128 values are valid serial numbers, so we can unwrap 123 | SerialNumber::new(&value.to_be_bytes()).unwrap() 124 | } 125 | } 126 | 127 | #[cfg(feature = "serde")] 128 | mod serde_support { 129 | use serde::de::Visitor; 130 | use serde::{Deserialize, Serialize}; 131 | 132 | use super::SerialNumber; 133 | 134 | struct SerialNumberVisitor; 135 | 136 | impl<'de> Visitor<'de> for SerialNumberVisitor { 137 | type Value = SerialNumber; 138 | 139 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 140 | formatter.write_str("a byte slice representing a positive integer") 141 | } 142 | 143 | fn visit_bytes(self, v: &[u8]) -> Result 144 | where 145 | E: serde::de::Error, 146 | { 147 | SerialNumber::new(v).map_err(serde::de::Error::custom) 148 | } 149 | 150 | fn visit_seq(self, mut seq: A) -> Result 151 | where 152 | A: serde::de::SeqAccess<'de>, 153 | { 154 | let mut bytes: Vec = Vec::new(); // Create a new Vec to store the bytes 155 | while let Some(byte) = seq.next_element()? { 156 | // "Iterate" over the sequence, assuming each element is a byte 157 | bytes.push(byte) // Push the byte to the Vec 158 | } 159 | SerialNumber::new(&bytes).map_err(serde::de::Error::custom) // Create a SerialNumber from the Vec 160 | } 161 | } 162 | 163 | impl<'de> Deserialize<'de> for SerialNumber { 164 | fn deserialize(deserializer: D) -> Result 165 | where 166 | D: serde::Deserializer<'de>, 167 | { 168 | deserializer.deserialize_any(SerialNumberVisitor) 169 | } 170 | } 171 | 172 | impl Serialize for SerialNumber { 173 | fn serialize(&self, serializer: S) -> Result 174 | where 175 | S: serde::Serializer, 176 | { 177 | serializer.serialize_bytes(self.as_bytes()) 178 | } 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod test { 184 | use log::trace; 185 | use serde_json::json; 186 | 187 | use crate::testing_utils::init_logger; 188 | 189 | use super::SerialNumber; 190 | 191 | #[test] 192 | fn serialize_deserialize() { 193 | init_logger(); 194 | let serial_number = SerialNumber::new(&2347812387874u128.to_be_bytes()).unwrap(); 195 | let serialized = json!(serial_number); 196 | trace!("is_array: {:?}", serialized.is_array()); 197 | trace!("serialized: {}", serialized); 198 | let deserialized: SerialNumber = serde_json::from_value(serialized).unwrap(); 199 | 200 | assert_eq!(serial_number, deserialized); 201 | } 202 | 203 | #[test] 204 | fn serial_number_from_to_u128() { 205 | init_logger(); 206 | let mut val = 0u128; 207 | loop { 208 | let serial_number = SerialNumber::new(&val.to_be_bytes()).unwrap(); 209 | let json = json!(serial_number); 210 | let deserialized: SerialNumber = serde_json::from_value(json).unwrap(); 211 | let u128 = deserialized.try_as_u128().unwrap(); 212 | assert_eq!(u128, val); 213 | assert_eq!(deserialized, serial_number); 214 | if val == 0 { 215 | val = 1; 216 | } 217 | if val == u128::MAX { 218 | break; 219 | } 220 | val = match val.checked_mul(2) { 221 | Some(v) => v, 222 | None => u128::MAX, 223 | }; 224 | } 225 | } 226 | 227 | #[test] 228 | fn try_as_u128() { 229 | init_logger(); 230 | let mut val = 1u128; 231 | loop { 232 | let serial_number = SerialNumber::new(&val.to_be_bytes()).unwrap(); 233 | let u128 = serial_number.try_as_u128().unwrap(); 234 | assert_eq!(u128, val); 235 | trace!("u128: {}", u128); 236 | if val == u128::MAX { 237 | break; 238 | } 239 | val = match val.checked_mul(2) { 240 | Some(v) => v, 241 | None => u128::MAX, 242 | }; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/wasm_bindgen/errors.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use wasm_bindgen::prelude::*; 6 | 7 | #[derive(Debug, Clone, Copy)] 8 | #[wasm_bindgen(js_name = "PolyprotoError")] 9 | pub enum JsConstraintError { 10 | InvalidInput, 11 | } 12 | -------------------------------------------------------------------------------- /src/wasm_bindgen/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | pub mod errors; 6 | pub mod types; 7 | 8 | mod utils; 9 | -------------------------------------------------------------------------------- /src/wasm_bindgen/types.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::types::DomainName as RDomainName; 6 | use crate::types::FederationId as RFederationId; 7 | use wasm_bindgen::prelude::*; 8 | 9 | use super::errors::JsConstraintError; 10 | 11 | #[derive(Clone, Debug)] 12 | #[wasm_bindgen(inspectable)] 13 | /// A `FederationId` is a globally unique identifier for an actor in the context of polyproto. 14 | pub struct FederationId { 15 | #[wasm_bindgen(skip)] 16 | _inner: RFederationId, 17 | } 18 | 19 | #[wasm_bindgen] 20 | impl FederationId { 21 | #[wasm_bindgen(constructor)] 22 | /// Validates input, then creates a new `FederationId`. Throws an error if input validation fails. 23 | pub fn new(id: &str) -> Result { 24 | Ok(FederationId { 25 | _inner: RFederationId::new(id).map_err(|_| JsConstraintError::InvalidInput)?, 26 | }) 27 | } 28 | 29 | #[wasm_bindgen(js_name = "toJSON")] 30 | pub fn js_to_json(&self) -> String { 31 | self._inner.to_string() 32 | } 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | #[wasm_bindgen(inspectable)] 37 | pub struct DomainName { 38 | #[wasm_bindgen(skip)] 39 | _inner: RDomainName, 40 | } 41 | -------------------------------------------------------------------------------- /src/wasm_bindgen/utils.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | pub fn set_panic_hook() { 6 | // When the `console_error_panic_hook` feature is enabled, we can call the 7 | // `set_panic_hook` function at least once during initialization, and then 8 | // we will get better error messages if our code ever panics. 9 | // 10 | // For more details see 11 | // https://github.com/rustwasm/console_error_panic_hook#readme 12 | #[cfg(feature = "console_error_panic_hook")] 13 | console_error_panic_hook::set_once(); 14 | } 15 | -------------------------------------------------------------------------------- /tests/api/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | pub(crate) mod core; 6 | -------------------------------------------------------------------------------- /tests/certs/capabilities/key_usage.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use der::asn1::BitString; 6 | use log::trace; 7 | use polyproto::certs::capabilities::{KeyUsage, KeyUsages}; 8 | 9 | use crate::common::init_logger; 10 | 11 | #[test] 12 | fn to_bitstring() { 13 | init_logger(); 14 | let key_usages_vec = vec![ 15 | KeyUsage::DigitalSignature, 16 | KeyUsage::ContentCommitment, 17 | KeyUsage::KeyEncipherment, 18 | KeyUsage::DataEncipherment, 19 | KeyUsage::KeyAgreement, 20 | KeyUsage::KeyCertSign, 21 | KeyUsage::CrlSign, 22 | KeyUsage::EncipherOnly, 23 | KeyUsage::DecipherOnly, 24 | ]; 25 | let key_usages = KeyUsages { 26 | key_usages: key_usages_vec, 27 | }; 28 | let bitstring = key_usages.to_bitstring(); 29 | trace!("Unused bits: {}", bitstring.unused_bits()); 30 | assert_eq!(bitstring.raw_bytes(), &[128, 255]); 31 | 32 | let key_usages_vec = vec![KeyUsage::DecipherOnly]; 33 | let key_usages = KeyUsages { 34 | key_usages: key_usages_vec, 35 | }; 36 | let bitstring = key_usages.to_bitstring(); 37 | trace!("Unused bits: {}", bitstring.unused_bits()); 38 | assert_eq!(bitstring.raw_bytes(), &[128, 0]); 39 | 40 | let key_usages_vec = vec![KeyUsage::DigitalSignature]; 41 | let key_usages = KeyUsages { 42 | key_usages: key_usages_vec, 43 | }; 44 | let bitstring = key_usages.to_bitstring(); 45 | trace!("Unused bits: {}", bitstring.unused_bits()); 46 | assert_eq!(bitstring.raw_bytes(), &[128]); 47 | 48 | let key_usages_vec = vec![ 49 | KeyUsage::DigitalSignature, 50 | KeyUsage::ContentCommitment, 51 | KeyUsage::KeyEncipherment, 52 | KeyUsage::DataEncipherment, 53 | KeyUsage::KeyAgreement, 54 | KeyUsage::KeyCertSign, 55 | KeyUsage::CrlSign, 56 | KeyUsage::EncipherOnly, 57 | ]; 58 | let key_usages = KeyUsages { 59 | key_usages: key_usages_vec, 60 | }; 61 | let bitstring = key_usages.to_bitstring(); 62 | trace!("Unused bits: {}", bitstring.unused_bits()); 63 | assert_eq!(bitstring.raw_bytes(), &[255]); 64 | } 65 | 66 | #[test] 67 | fn from_bitstring() { 68 | let bitstring = BitString::new(7, [128, 255]).unwrap(); 69 | let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); 70 | key_usages.key_usages.sort(); 71 | let mut expected = [ 72 | KeyUsage::DigitalSignature, 73 | KeyUsage::ContentCommitment, 74 | KeyUsage::KeyEncipherment, 75 | KeyUsage::DataEncipherment, 76 | KeyUsage::KeyAgreement, 77 | KeyUsage::KeyCertSign, 78 | KeyUsage::CrlSign, 79 | KeyUsage::EncipherOnly, 80 | KeyUsage::DecipherOnly, 81 | ]; 82 | expected.sort(); 83 | assert_eq!(key_usages.key_usages, expected); 84 | 85 | let bitstring = BitString::new(7, [128, 0]).unwrap(); 86 | let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); 87 | key_usages.key_usages.sort(); 88 | let mut expected = [KeyUsage::DecipherOnly]; 89 | expected.sort(); 90 | assert_eq!(key_usages.key_usages, expected); 91 | 92 | let bitstring = BitString::new(0, [128]).unwrap(); 93 | let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); 94 | key_usages.key_usages.sort(); 95 | let mut expected = [KeyUsage::DigitalSignature]; 96 | expected.sort(); 97 | assert_eq!(key_usages.key_usages, expected); 98 | 99 | let bitstring = BitString::new(0, [255]).unwrap(); 100 | let mut key_usages = KeyUsages::from_bitstring(bitstring).unwrap(); 101 | key_usages.key_usages.sort(); 102 | let mut expected = [ 103 | KeyUsage::DigitalSignature, 104 | KeyUsage::ContentCommitment, 105 | KeyUsage::KeyEncipherment, 106 | KeyUsage::DataEncipherment, 107 | KeyUsage::KeyAgreement, 108 | KeyUsage::KeyCertSign, 109 | KeyUsage::CrlSign, 110 | KeyUsage::EncipherOnly, 111 | ]; 112 | expected.sort(); 113 | assert_eq!(key_usages.key_usages, expected); 114 | } 115 | -------------------------------------------------------------------------------- /tests/certs/capabilities/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | mod key_usage; 6 | -------------------------------------------------------------------------------- /tests/certs/idcsr.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | // This Source Code Form is subject to the terms of the Mozilla Public 5 | // License, v. 2.0. If a copy of the MPL was not distributed with this 6 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | 8 | #![allow(unused)] 9 | 10 | use std::str::FromStr; 11 | use std::time::Duration; 12 | 13 | use crate::common::*; 14 | use der::asn1::{BitString, Ia5String, Uint, UtcTime}; 15 | use ed25519_dalek::{Signature as Ed25519DalekSignature, Signer, SigningKey, VerifyingKey}; 16 | use polyproto::certs::capabilities::{self, Capabilities}; 17 | use polyproto::certs::idcert::IdCert; 18 | use polyproto::certs::idcsr::IdCsr; 19 | use polyproto::certs::{PublicKeyInfo, Target}; 20 | use polyproto::key::{PrivateKey, PublicKey}; 21 | use polyproto::signature::Signature; 22 | use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SignatureBitStringEncoding}; 23 | use thiserror::Error; 24 | use x509_cert::Certificate; 25 | use x509_cert::attr::Attributes; 26 | use x509_cert::name::RdnSequence; 27 | use x509_cert::request::CertReq; 28 | use x509_cert::time::{Time, Validity}; 29 | 30 | test_all_platforms! { 31 | fn csr_from_pem() { 32 | init_logger(); 33 | let mut csprng = rand::rngs::OsRng; 34 | let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 35 | 36 | let csr = polyproto::certs::idcsr::IdCsr::new( 37 | &RdnSequence::from_str( 38 | "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", 39 | ) 40 | .unwrap(), 41 | &priv_key, 42 | &Capabilities::default_actor(), 43 | Some(Target::Actor), 44 | ) 45 | .unwrap(); 46 | let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); 47 | let csr_from_pem = IdCsr::from_pem(&data, Some(polyproto::certs::Target::Actor)).unwrap(); 48 | assert_eq!(csr_from_pem, csr); 49 | 50 | let csr = polyproto::certs::idcsr::IdCsr::new( 51 | &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), 52 | &priv_key, 53 | &Capabilities::default_home_server(), 54 | Some(Target::HomeServer), 55 | ) 56 | .unwrap(); 57 | let data = csr.clone().to_pem(der::pem::LineEnding::LF).unwrap(); 58 | let csr_from_pem = IdCsr::from_pem(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); 59 | assert_eq!(csr_from_pem, csr); 60 | } 61 | } 62 | 63 | test_all_platforms! { 64 | fn test_create_invalid_actor_csr() { 65 | init_logger(); 66 | let mut csprng = rand::rngs::OsRng; 67 | let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 68 | println!("Private Key is: {:?}", priv_key.key.to_bytes()); 69 | println!("Public Key is: {:?}", priv_key.public_key.key.to_bytes()); 70 | println!(); 71 | 72 | let mut capabilities = Capabilities::default_actor(); 73 | // This is not allowed in actor certificates/csrs 74 | capabilities 75 | .key_usage 76 | .key_usages 77 | .push(capabilities::KeyUsage::KeyCertSign); 78 | 79 | let csr = polyproto::certs::idcsr::IdCsr::new( 80 | &RdnSequence::from_str( 81 | "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", 82 | ) 83 | .unwrap(), 84 | &priv_key, 85 | &capabilities, 86 | Some(Target::Actor), 87 | ); 88 | assert!(csr.is_err()); 89 | } 90 | } 91 | 92 | test_all_platforms! { 93 | fn csr_from_der() { 94 | init_logger(); 95 | let mut csprng = rand::rngs::OsRng; 96 | let priv_key = Ed25519PrivateKey::gen_keypair(&mut csprng); 97 | 98 | let csr = polyproto::certs::idcsr::IdCsr::new( 99 | &RdnSequence::from_str( 100 | "CN=flori,DC=polyphony,DC=chat,UID=flori@polyphony.chat,uniqueIdentifier=client1", 101 | ) 102 | .unwrap(), 103 | &priv_key, 104 | &Capabilities::default_actor(), 105 | Some(Target::Actor), 106 | ) 107 | .unwrap(); 108 | let data = csr.clone().to_der().unwrap(); 109 | let csr_from_der = IdCsr::from_der(&data, Some(polyproto::certs::Target::Actor)).unwrap(); 110 | assert_eq!(csr_from_der, csr); 111 | 112 | let csr = polyproto::certs::idcsr::IdCsr::new( 113 | &RdnSequence::from_str("CN=root,DC=polyphony,DC=chat").unwrap(), 114 | &priv_key, 115 | &Capabilities::default_home_server(), 116 | Some(Target::HomeServer), 117 | ) 118 | .unwrap(); 119 | let data = csr.clone().to_der().unwrap(); 120 | let csr_from_der = IdCsr::from_der(&data, Some(polyproto::certs::Target::HomeServer)).unwrap(); 121 | assert_eq!(csr_from_der, csr); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/certs/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | mod capabilities; 6 | mod idcert; 7 | mod idcsr; 8 | -------------------------------------------------------------------------------- /tests/gateway/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::common::{Ed25519PrivateKey, Ed25519Signature}; 6 | 7 | #[cfg(not(target_arch = "wasm32"))] 8 | #[tokio::test] 9 | async fn connect_tungstenite_hello() { 10 | use std::sync::Arc; 11 | 12 | use polyproto::api::{HttpClient, Session}; 13 | use polyproto::gateway::{BackendBehavior, backends}; 14 | use polyproto::types::gateway::payload::Hello; 15 | use polyproto::types::gateway::{CoreEvent, Payload}; 16 | use serde_json::json; 17 | use tokio_tungstenite::tungstenite::Message; 18 | use url::Url; 19 | use ws_mock::ws_mock_server::{WsMock, WsMockServer}; 20 | 21 | use crate::common; 22 | 23 | let server = WsMockServer::start().await; 24 | let (send, receive) = tokio::sync::mpsc::channel::(32); 25 | WsMock::new() 26 | .forward_from_channel(receive) 27 | .mount(&server) 28 | .await; 29 | 30 | common::init_logger(); 31 | let tungstenite = backends::tungstenite::TungsteniteBackend::new(); 32 | let session = Session::::new( 33 | &HttpClient::new().unwrap(), 34 | "none", 35 | Url::parse("http://127.0.0.1").unwrap(), 36 | None, 37 | ); 38 | send.send(Message::Text( 39 | json!(CoreEvent::new( 40 | Payload::Hello(Hello { 41 | heartbeat_interval: 12345, 42 | active_migration: None 43 | }), 44 | None 45 | )) 46 | .to_string() 47 | .into(), 48 | )) 49 | .await 50 | .unwrap(); 51 | println!("sent data"); 52 | tungstenite 53 | .connect(Arc::new(session), &Url::parse(&server.uri().await).unwrap()) 54 | .await 55 | .unwrap(); 56 | } 57 | 58 | #[cfg(not(target_arch = "wasm32"))] 59 | #[tokio::test] 60 | async fn respond_to_manual_heartbeat() { 61 | use std::sync::Arc; 62 | use std::time::Duration; 63 | 64 | use polyproto::api::{HttpClient, Session}; 65 | use polyproto::gateway::{BackendBehavior, backends}; 66 | use polyproto::types::gateway::payload::{Heartbeat, Hello}; 67 | use polyproto::types::gateway::{CoreEvent, Payload}; 68 | use serde_json::json; 69 | use tokio_tungstenite::tungstenite::Message; 70 | use url::Url; 71 | use ws_mock::matchers::JsonExact; 72 | use ws_mock::ws_mock_server::{WsMock, WsMockServer}; 73 | 74 | use crate::common; 75 | 76 | let server = WsMockServer::start().await; 77 | let (send, receive) = tokio::sync::mpsc::channel::(32); 78 | WsMock::new() 79 | .forward_from_channel(receive) 80 | .mount(&server) 81 | .await; 82 | 83 | common::init_logger(); 84 | let tungstenite = backends::tungstenite::TungsteniteBackend::new(); 85 | let session = Session::::new( 86 | &HttpClient::new().unwrap(), 87 | "none", 88 | Url::parse("http://127.0.0.1").unwrap(), 89 | None, 90 | ); 91 | dbg!(json!(CoreEvent::new( 92 | Payload::Hello(Hello { 93 | heartbeat_interval: 12345, 94 | active_migration: None 95 | }), 96 | None 97 | ))); 98 | send.send(Message::Text( 99 | json!(CoreEvent::new( 100 | Payload::Hello(Hello { 101 | heartbeat_interval: 12345, 102 | active_migration: None 103 | }), 104 | None 105 | )) 106 | .to_string() 107 | .into(), 108 | )) 109 | .await 110 | .unwrap(); 111 | println!("sent hello data"); 112 | tungstenite 113 | .connect(Arc::new(session), &Url::parse(&server.uri().await).unwrap()) 114 | .await 115 | .unwrap(); 116 | WsMock::new() 117 | .matcher(JsonExact::new(json!(CoreEvent::new( 118 | Payload::Heartbeat(Heartbeat { 119 | from: 0, 120 | to: 0, 121 | except: vec![] 122 | }), 123 | None 124 | )))) 125 | .expect(1) 126 | .mount(&server) 127 | .await; 128 | tokio::time::sleep(Duration::from_millis(100)).await; 129 | dbg!(json!(CoreEvent::new(Payload::RequestHeartbeat, None))); 130 | send.send(Message::Text( 131 | json!(CoreEvent::new(Payload::RequestHeartbeat, None)) 132 | .to_string() 133 | .into(), 134 | )) 135 | .await 136 | .unwrap(); 137 | println!("sent heartbeat req"); 138 | tokio::time::sleep(Duration::from_millis(100)).await; 139 | server.verify().await; 140 | } 141 | -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use polyproto::errors::CertificateConversionError; 6 | 7 | #[cfg(feature = "reqwest")] 8 | pub(crate) mod api; 9 | pub(crate) mod certs; 10 | pub(crate) mod common; 11 | #[cfg(feature = "gateway")] 12 | pub(crate) mod gateway; 13 | #[cfg(feature = "types")] 14 | pub(crate) mod types; 15 | 16 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 17 | #[cfg_attr(not(target_arch = "wasm32"), test)] 18 | fn conversion_error_from_oid_error() { 19 | let oid_error = 20 | der::oid::ObjectIdentifier::new("this isn't valid in any sense of the imagination") 21 | .unwrap_err(); 22 | let our_error = CertificateConversionError::from(oid_error); 23 | let inner_error = match our_error { 24 | CertificateConversionError::ConstOidError(error) => error, 25 | _ => panic!(), 26 | }; 27 | assert_eq!(inner_error, oid_error) 28 | } 29 | -------------------------------------------------------------------------------- /tests/types/federation_id.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use polyproto::types::{DomainName, FederationId}; 6 | use serde_json::{Value, from_value, to_value}; 7 | 8 | #[cfg(feature = "types")] 9 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 10 | #[cfg_attr(not(target_arch = "wasm32"), test)] 11 | fn test_correct_federation_ids() { 12 | assert!(FederationId::new(" xenia@examle").is_err()); 13 | assert!(FederationId::new("xenia@example").is_ok()); 14 | assert!(FederationId::new("xenia@").is_err()); 15 | assert!(FederationId::new("@example.com").is_err()); 16 | assert!(FederationId::new("xenia.example.com").is_err()); 17 | assert!(FederationId::new("xenia@example.com/").is_err()); 18 | assert!(FederationId::new("xenia@example.com/1").is_err()); 19 | assert!(FederationId::new("xenia@_example.com").is_err()); 20 | assert!(FederationId::new("xenia@xn--grn-ioaaa.de").is_ok()); 21 | FederationId::new("flori@polyphony.chat").unwrap(); 22 | FederationId::new("a@localhost").unwrap(); 23 | FederationId::new("really-long.domain.with-at-least-4-subdomains.or-something@example.com") 24 | .unwrap(); 25 | assert!(FederationId::new("example@xn--638h.com").is_ok()); 26 | assert!(FederationId::new("\\@example.com").is_err()); 27 | assert!(FederationId::new("example.com").is_err()); 28 | assert!(FederationId::new("examplecom").is_err()); 29 | assert!(FederationId::new("⾆@example.com").is_err()); 30 | assert!(FederationId::new("example@⾆.com").is_err()); 31 | assert!(FederationId::new("example@😿.com").is_err()); 32 | } 33 | 34 | #[cfg(feature = "serde")] 35 | #[cfg(feature = "types")] 36 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 37 | #[cfg_attr(not(target_arch = "wasm32"), test)] 38 | fn test_fid_serde_serialize() { 39 | const NAME: &str = "xenia@example.com"; 40 | let id = FederationId::new(NAME).unwrap(); 41 | let json = to_value(id).unwrap(); 42 | assert!(json.is_string()); 43 | assert_eq!(json, Value::String(NAME.to_string())) 44 | } 45 | 46 | #[cfg(feature = "serde")] 47 | #[cfg(feature = "types")] 48 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 49 | #[cfg_attr(not(target_arch = "wasm32"), test)] 50 | fn test_fid_serde_deserialize() { 51 | use serde_json::from_value; 52 | 53 | const NAME: &str = "xenia@example.com"; 54 | let json_value = Value::String(NAME.to_string()); 55 | let id: FederationId = from_value(json_value).unwrap(); 56 | assert_eq!(&id.to_string(), NAME); 57 | } 58 | 59 | #[cfg(feature = "serde")] 60 | #[cfg(feature = "types")] 61 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 62 | #[cfg_attr(not(target_arch = "wasm32"), test)] 63 | fn test_domain_name_serde_serialize() { 64 | const NAME: &str = "xenia@example.com"; 65 | let domain_name = DomainName::new(NAME).unwrap(); 66 | let json = to_value(domain_name).unwrap(); 67 | assert!(json.is_string()); 68 | assert_eq!(json, Value::String(NAME.to_string())); 69 | } 70 | 71 | #[cfg(feature = "serde")] 72 | #[cfg(feature = "types")] 73 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 74 | #[cfg_attr(not(target_arch = "wasm32"), test)] 75 | fn test_domain_name_serde_deserialize() { 76 | const NAME: &str = "xenia@example.com"; 77 | let json_value = Value::String(NAME.to_string()); 78 | let domain_name: DomainName = from_value(json_value).unwrap(); 79 | assert_eq!(domain_name.to_string().as_str(), NAME); 80 | } 81 | 82 | #[cfg(feature = "serde")] 83 | #[cfg(feature = "types")] 84 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 85 | #[cfg_attr(not(target_arch = "wasm32"), test)] 86 | fn test_serde_serialize_identifier() { 87 | use polyproto::types::Identifer; 88 | 89 | const DOMAIN_NAME: &str = "example.com"; 90 | const FEDERATION_ID: &str = "user@example.com"; 91 | 92 | let instance_id = Identifer::Instance(DomainName::new(DOMAIN_NAME).unwrap()); 93 | let federation_id = Identifer::FederationId(FederationId::new(FEDERATION_ID).unwrap()); 94 | 95 | let instance_json = to_value(instance_id).unwrap(); 96 | let federation_json = to_value(federation_id).unwrap(); 97 | 98 | assert!(instance_json.is_string()); 99 | assert_eq!(instance_json, Value::String(DOMAIN_NAME.to_string())); 100 | 101 | assert!(federation_json.is_string()); 102 | assert_eq!(federation_json, Value::String(FEDERATION_ID.to_string())); 103 | } 104 | 105 | #[cfg(feature = "serde")] 106 | #[cfg(feature = "types")] 107 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 108 | #[cfg_attr(not(target_arch = "wasm32"), test)] 109 | fn test_serde_deserialize_identifier() { 110 | use polyproto::types::Identifer; 111 | 112 | const DOMAIN_NAME: &str = "example.com"; 113 | const FEDERATION_ID: &str = "user@example.com"; 114 | 115 | let instance_json_value = Value::String(DOMAIN_NAME.to_string()); 116 | let federation_json_value = Value::String(FEDERATION_ID.to_string()); 117 | 118 | let instance_id: Identifer = from_value(instance_json_value).unwrap(); 119 | let federation_id: Identifer = from_value(federation_json_value).unwrap(); 120 | 121 | match instance_id { 122 | Identifer::Instance(dn) => assert_eq!(dn.to_string().as_str(), DOMAIN_NAME), 123 | _ => panic!("Expected an Instance variant"), 124 | } 125 | 126 | match federation_id { 127 | Identifer::FederationId(fid) => assert_eq!(fid.to_string().as_str(), FEDERATION_ID), 128 | _ => panic!("Expected a FederationId variant"), 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/types/keys.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use polyproto::key::PublicKey; 6 | use polyproto::signature::Signature; 7 | 8 | use crate::common::{self, Ed25519Signature}; 9 | 10 | #[test] 11 | fn eq_algorithm_identifier_signature_publickey() { 12 | let private_key = common::gen_priv_key(); 13 | let public_key = private_key.public_key.clone(); 14 | assert_eq!( 15 | Ed25519Signature::algorithm_identifier(), 16 | public_key.algorithm_identifier() 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /tests/types/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | mod cacheable_cert; 6 | mod federation_id; 7 | #[cfg(feature = "gateway")] 8 | mod gateway; 9 | mod keys; 10 | -------------------------------------------------------------------------------- /third_party/tungstenite/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Alexey Galakhov 2 | Copyright (c) 2016 Jason Housley 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. --------------------------------------------------------------------------------