├── .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 |
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("coolname").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.
--------------------------------------------------------------------------------