rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodf8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "24e3c53626bd5e1d4a71c188e8d897f47eddbbcb",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "crypto: Rewrite",
"state": {
"status": "open",
"conflicts": []
},
"before": "027de89cdb93f9f08e46ab76ab38c62c4005724a",
"after": "f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da",
"commits": [
"f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da",
"c81ffc0ec14c5c25400025e26542f3b33f3e5685",
"9cfaa2bd7d14855213f60bb0e9a2f008892a0a5a"
],
"target": "027de89cdb93f9f08e46ab76ab38c62c4005724a",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "24e3c53626bd5e1d4a71c188e8d897f47eddbbcb",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Reasons for the rewrite:\n - Migrate away from `ec25519` as our implementation of Ed25519,\n because that crate is unmaintained.\n Instead of `ec25519` we use `ed25519-dalek` which is the most popular\n implementation of Ed25519 in Rust.\n - Migrate away from `amplify`, because we make very little use of its\n features. Here, we only use `amplify::Bytes32`, which is just a thin\n wrapper of `[u8; 32]`.\n Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`.\n\nThe realization that `ExtendedSignature` is not really tied to SSH, but\nis merely a representation of a key together with a signature, allows to\nrelax the bounds on traits in `radicle-cob`.\n\nSeparating out uses of \"public key\" (a rather simple model of a key,\njust 32 bytes) and \"verifying key\" (an actual point on the Edwards\ncurve, ready to verify signatures, using more memory than just 32 bytes)\nallows a more sensible definition of `crypto::Signer`, which propagates\nto `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and\nsimplifies trait bounds.\n\nIn `radicle-node`, make the fact clearer that actually the secret key\nitself is required (for our implementation of Diffie-Hellman) not\n\"just\" signing. This results in removal of the generic argument\n(usually called `G`) of multiple types.\n\nConcretizing the types in `radicle-node` this way, i.e. by (morally)\nsetting `G = radicle_crypto::SigningKey`, leads to removal of\n`MemorySigner` and `MockSigner`, as they really just wrap a signing\nkey.\n\nRe-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`)\nis hopefully more future-proof than using our own type for signatures.\nHowever, the fact that `ed25519::Signature` is not serializable and\ndoes not implement `std::hash::Hash` means that we have to resort to\nad-hoc solutions in `radicle`:\n 1. The Identity COB requires serialization of signatures. These are\n generally questionable, since the security of the Identity COB\n should rely on the security of Signed References. In the medium\n term we might even want to consider ignoring signatures in actions.\n 2. When reading Signed References, detection of Replay Attacks uses\n a `HashMap` keyed by signatures. This detection only exists for\n backwards compatibility.\nThese two use cases are exotic enough to warrant ad-hoc implementations.\nBy not wrapping `ed25519::Signature`, we encourage writing more widely\ncompatible code in the future.",
"base": "027de89cdb93f9f08e46ab76ab38c62c4005724a",
"oid": "f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da",
"timestamp": 1784284925
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "d0edeb0a-e1bd-41d1-bdaa-bf310d599c16"
},
"info_url": "https://cci.rad.levitte.org//d0edeb0a-e1bd-41d1-bdaa-bf310d599c16.html"
}
Started at: 2026-07-17 12:43:03.722020+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 187 issues · 42 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb
✓ Switched to branch patch/24e3c53 at revision 24e3c53
✓ Branch patch/24e3c53 setup to track rad/patches/24e3c53626bd5e1d4a71c188e8d897f47eddbbcb
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da
HEAD is now at f8d4ff1f crypto: Rewrite
Exit code: 0
$ rad patch show 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb -p
╭──────────────────────────────────────────────────────────────────────────────╮
│ Title crypto: Rewrite │
│ Patch 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da │
│ Base 027de89cdb93f9f08e46ab76ab38c62c4005724a │
│ Branches patch/24e3c53 │
│ Commits ahead 3, behind 0 │
│ Status open │
│ │
│ Reasons for the rewrite: │
│ - Migrate away from `ec25519` as our implementation of Ed25519, │
│ because that crate is unmaintained. │
│ Instead of `ec25519` we use `ed25519-dalek` which is the most popular │
│ implementation of Ed25519 in Rust. │
│ - Migrate away from `amplify`, because we make very little use of its │
│ features. Here, we only use `amplify::Bytes32`, which is just a thin │
│ wrapper of `[u8; 32]`. │
│ Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`. │
│ │
│ The realization that `ExtendedSignature` is not really tied to SSH, but │
│ is merely a representation of a key together with a signature, allows to │
│ relax the bounds on traits in `radicle-cob`. │
│ │
│ Separating out uses of "public key" (a rather simple model of a key, │
│ just 32 bytes) and "verifying key" (an actual point on the Edwards │
│ curve, ready to verify signatures, using more memory than just 32 bytes) │
│ allows a more sensible definition of `crypto::Signer`, which propagates │
│ to `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and │
│ simplifies trait bounds. │
│ │
│ In `radicle-node`, make the fact clearer that actually the secret key │
│ itself is required (for our implementation of Diffie-Hellman) not │
│ "just" signing. This results in removal of the generic argument │
│ (usually called `G`) of multiple types. │
│ │
│ Concretizing the types in `radicle-node` this way, i.e. by (morally) │
│ setting `G = radicle_crypto::SigningKey`, leads to removal of │
│ `MemorySigner` and `MockSigner`, as they really just wrap a signing │
│ key. │
│ │
│ Re-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`) │
│ is hopefully more future-proof than using our own type for signatures. │
│ However, the fact that `ed25519::Signature` is not serializable and │
│ does not implement `std::hash::Hash` means that we have to resort to │
│ ad-hoc solutions in `radicle`: │
│ 1. The Identity COB requires serialization of signatures. These are │
│ generally questionable, since the security of the Identity COB │
│ should rely on the security of Signed References. In the medium │
│ term we might even want to consider ignoring signatures in actions. │
│ 2. When reading Signed References, detection of Replay Attacks uses │
│ a `HashMap` keyed by signatures. This detection only exists for │
│ backwards compatibility. │
│ These two use cases are exotic enough to warrant ad-hoc implementations. │
│ By not wrapping `ed25519::Signature`, we encourage writing more widely │
│ compatible code in the future. │
├──────────────────────────────────────────────────────────────────────────────┤
│ f8d4ff1 crypto: Rewrite │
│ c81ffc0 radicle, cli/auth: Let binary crate generate seed │
│ 9cfaa2b crypto/test: Diffie-Hellman Fixture │
├──────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 24e3c53 @ 027de89..f8d4ff1 by lorenz z6MkkPv…WX5sTEz 1 minute ago │
╰──────────────────────────────────────────────────────────────────────────────╯
commit f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Mon Jun 29 14:06:31 2026 +0200
crypto: Rewrite
Reasons for the rewrite:
- Migrate away from `ec25519` as our implementation of Ed25519,
because that crate is unmaintained.
Instead of `ec25519` we use `ed25519-dalek` which is the most popular
implementation of Ed25519 in Rust.
- Migrate away from `amplify`, because we make very little use of its
features. Here, we only use `amplify::Bytes32`, which is just a thin
wrapper of `[u8; 32]`.
Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`.
The realization that `ExtendedSignature` is not really tied to SSH, but
is merely a representation of a key together with a signature, allows to
relax the bounds on traits in `radicle-cob`.
Separating out uses of "public key" (a rather simple model of a key,
just 32 bytes) and "verifying key" (an actual point on the Edwards
curve, ready to verify signatures, using more memory than just 32 bytes)
allows a more sensible definition of `crypto::Signer`, which propagates
to `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and
simplifies trait bounds.
In `radicle-node`, make the fact clearer that actually the secret key
itself is required (for our implementation of Diffie-Hellman) not
"just" signing. This results in removal of the generic argument
(usually called `G`) of multiple types.
Concretizing the types in `radicle-node` this way, i.e. by (morally)
setting `G = radicle_crypto::SigningKey`, leads to removal of
`MemorySigner` and `MockSigner`, as they really just wrap a signing
key.
Re-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`)
is hopefully more future-proof than using our own type for signatures.
However, the fact that `ed25519::Signature` is not serializable and
does not implement `std::hash::Hash` means that we have to resort to
ad-hoc solutions in `radicle`:
1. The Identity COB requires serialization of signatures. These are
generally questionable, since the security of the Identity COB
should rely on the security of Signed References. In the medium
term we might even want to consider ignoring signatures in actions.
2. When reading Signed References, detection of Replay Attacks uses
a `HashMap` keyed by signatures. This detection only exists for
backwards compatibility.
These two use cases are exotic enough to warrant ad-hoc implementations.
By not wrapping `ed25519::Signature`, we encourage writing more widely
compatible code in the future.
diff --git a/Cargo.lock b/Cargo.lock
index f2e73a09c..d77cc6d34 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -671,12 +671,6 @@ dependencies = [
"typenum",
]
-[[package]]
-name = "ct-codecs"
-version = "1.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
-
[[package]]
name = "ctr"
version = "0.9.2"
@@ -698,7 +692,9 @@ dependencies = [
"digest",
"fiat-crypto",
"rustc_version",
+ "serde",
"subtle",
+ "zeroize",
]
[[package]]
@@ -733,6 +729,7 @@ checksum = "f23b157ed1e424149e1af82c6ed8300d41941756f5e9841396fd987d946ab0fe"
dependencies = [
"amplify",
"ec25519",
+ "multibase",
"sha2",
]
@@ -884,7 +881,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfd533a2fc01178c738c99412ae1f7e1ad2cb37c2e14bfd87e9d4618171c825"
dependencies = [
- "ct-codecs",
"ed25519 1.5.3",
"getrandom 0.2.17",
]
@@ -918,6 +914,8 @@ version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
+ "pkcs8",
+ "serde",
"signature 2.2.0",
]
@@ -929,8 +927,10 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519 2.2.3",
+ "serde",
"sha2",
"subtle",
+ "zeroize",
]
[[package]]
@@ -2881,6 +2881,7 @@ dependencies = [
"jsonschema",
"libc",
"log",
+ "multibase",
"nonempty 0.9.0",
"pretty_assertions",
"qcheck",
@@ -2985,7 +2986,6 @@ dependencies = [
"radicle-oid",
"serde",
"serde_json",
- "signature 2.2.0",
"tempfile",
"thiserror 2.0.18",
]
@@ -3013,23 +3013,22 @@ dependencies = [
name = "radicle-crypto"
version = "0.17.0"
dependencies = [
- "amplify",
+ "curve25519-dalek",
"cyphernet",
- "ec25519",
- "fastrand",
+ "ed25519-dalek",
"git-ref-format-core",
"multibase",
"qcheck",
"qcheck-macros",
"schemars",
"serde",
- "signature 2.2.0",
"sqlite",
"ssh-agent-lib",
"ssh-key",
"tempfile",
"thiserror 2.0.18",
"winpipe",
+ "x25519-dalek",
"zeroize",
]
@@ -4196,7 +4195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -4556,9 +4555,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
-version = "1.19.0"
+version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
@@ -5240,6 +5239,18 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+[[package]]
+name = "x25519-dalek"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.6.4",
+ "serde",
+ "zeroize",
+]
+
[[package]]
name = "xattr"
version = "1.6.1"
@@ -5322,9 +5333,24 @@ dependencies = [
[[package]]
name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "serde",
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
name = "zerotrie"
diff --git a/Cargo.toml b/Cargo.toml
index 48ca6caba..e88f1a671 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,6 @@ version = "0.9.0"
rust-version = "1.88.0"
[workspace.dependencies]
-amplify = { version = "4.0.0", default-features = false }
backtrace = "0.3.75"
bstr = "1.3"
bytes = "1.11.1"
@@ -39,7 +38,7 @@ itertools = "0.14"
lexopt = "0.3.0"
libc = "0.2.137"
log = "0.4.17"
-multibase = "0.9.1"
+multibase = { version = "0.9.1", default-features = false }
nonempty = "0.9.0"
pretty_assertions = "1.3.0"
proptest = "1.9"
@@ -68,7 +67,6 @@ schemars = { version = "1.0.4", default-features = false }
serde = { version = "1.0", default-features = false }
serde_json = "1.0"
shlex = "1.1.0"
-signature = "2.2"
snapbox = "1.2"
sqlite = "0.37.0"
tempfile = "3.3.0"
@@ -77,7 +75,7 @@ uds_windows = "1.1.0"
windows = "0.62"
winpipe = "0.1.1"
winsplit = "0.1.0"
-zeroize = "1.5.7"
+zeroize = { version = "1.5.7", default-features = false }
# Crates from the "radicle-git" workspace. These should be synced manually.
# When updating, start from `radicle-surf`:
diff --git a/crates/radicle-cli/examples/rad-clone-partial-fail.md b/crates/radicle-cli/examples/rad-clone-partial-fail.md
index 18af1cfd8..44e6ec380 100644
--- a/crates/radicle-cli/examples/rad-clone-partial-fail.md
+++ b/crates/radicle-cli/examples/rad-clone-partial-fail.md
@@ -6,7 +6,7 @@ $ rad node routing
│ RID NID │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
-│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT │
+│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT │
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
╰──────────────────────────────────────────────────────────────────────────────────────╯
```
diff --git a/crates/radicle-cli/src/commands/auth.rs b/crates/radicle-cli/src/commands/auth.rs
index ca9374228..4caed9f02 100644
--- a/crates/radicle-cli/src/commands/auth.rs
+++ b/crates/radicle-cli/src/commands/auth.rs
@@ -4,8 +4,7 @@ use std::str::FromStr;
use anyhow::{Context, anyhow};
-use radicle::crypto::ssh;
-use radicle::crypto::ssh::Passphrase;
+use radicle::crypto;
use radicle::node::Alias;
use radicle::profile::env;
use radicle::{Profile, profile};
@@ -72,7 +71,7 @@ pub fn init(args: Args) -> anyhow::Result<()> {
spinner.finish();
if let Some(passphrase) = passphrase {
- match ssh::agent::Agent::connect() {
+ match crypto::ssh::agent::Agent::connect() {
Ok(mut agent) => {
let mut spinner = term::spinner("Adding your Radicle key to ssh-agent…");
if register(&mut agent, &profile, passphrase).is_ok() {
@@ -133,9 +132,9 @@ pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
// If our key is encrypted, we try to authenticate with SSH Agent and
// register it; only if it is running.
- match ssh::agent::Agent::connect() {
+ match crypto::ssh::agent::Agent::connect() {
Ok(mut agent) => {
- if agent.request_identities()?.contains(&profile.public_key) {
+ if agent.request_identities()?.contains(profile.id()) {
term::success!("Radicle key already in ssh-agent");
return Ok(());
}
@@ -164,7 +163,7 @@ pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
// Try RAD_PASSPHRASE fallback.
if let Some(passphrase) = profile::env::passphrase() {
- ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase))
+ crypto::SigningKey::load(&profile.keystore, Some(passphrase))
.map_err(|_| anyhow!("`{}` is invalid", env::RAD_PASSPHRASE))?;
return Ok(());
}
@@ -181,9 +180,9 @@ pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
/// Register key with ssh-agent.
pub fn register(
- agent: &mut ssh::agent::Agent,
+ agent: &mut crypto::ssh::agent::Agent,
profile: &Profile,
- passphrase: Passphrase,
+ passphrase: crypto::ssh::Passphrase,
) -> anyhow::Result<()> {
let secret = profile
.keystore
diff --git a/crates/radicle-cli/src/commands/id.rs b/crates/radicle-cli/src/commands/id.rs
index 5af4fb60b..e4e0f10a0 100644
--- a/crates/radicle-cli/src/commands/id.rs
+++ b/crates/radicle-cli/src/commands/id.rs
@@ -442,20 +442,16 @@ and description.
Ok(result)
}
-fn update<Repo, Signer>(
+fn update(
title: Option<Title>,
description: Option<String>,
doc: Doc,
- current: &mut IdentityMut<Repo, Signer>,
+ current: &mut IdentityMut<
+ impl WriteRepository + cob::Store<Namespace = NodeId>,
+ impl crypto::Signer,
+ >,
profile: &Profile,
-) -> anyhow::Result<Revision>
-where
- Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<Revision> {
if let Some((title, description)) = edit_title_description(title, description)? {
let id = current
.update(title, description, &doc)
diff --git a/crates/radicle-cli/src/commands/inbox.rs b/crates/radicle-cli/src/commands/inbox.rs
index b92935ebb..1395f55a1 100644
--- a/crates/radicle-cli/src/commands/inbox.rs
+++ b/crates/radicle-cli/src/commands/inbox.rs
@@ -8,7 +8,7 @@ use std::process;
use localtime::LocalTime;
use radicle::cob::TypedId;
use radicle::git::BranchName;
-use radicle::git::fmt::Qualified;
+use radicle::git::fmt::{Component, Qualified};
use radicle::identity::Identity;
use radicle::issue::cache::Issues as _;
use radicle::node::notifications;
@@ -438,7 +438,7 @@ fn show(
NotificationKind::Branch { .. } => {
let refstr = if let Some(remote) = n.remote {
n.qualified
- .with_namespace(remote.to_component())
+ .with_namespace(Component::from(&remote))
.to_string()
} else {
n.qualified.to_string()
diff --git a/crates/radicle-cli/src/commands/init.rs b/crates/radicle-cli/src/commands/init.rs
index 7774189e6..2e52e7f53 100644
--- a/crates/radicle-cli/src/commands/init.rs
+++ b/crates/radicle-cli/src/commands/init.rs
@@ -240,7 +240,7 @@ pub fn init_existing(
&working,
&radicle::rad::REMOTE_NAME,
&url,
- &url.clone().with_namespace(profile.public_key),
+ &url.clone().with_namespace(*profile.id()),
)?;
if args.set_upstream {
diff --git a/crates/radicle-cli/src/commands/issue.rs b/crates/radicle-cli/src/commands/issue.rs
index c52df0f68..dc42414b7 100644
--- a/crates/radicle-cli/src/commands/issue.rs
+++ b/crates/radicle-cli/src/commands/issue.rs
@@ -354,23 +354,21 @@ fn mk_issue_row(
]
}
-fn open<Repo, Signer>(
+fn open(
title: Option<Title>,
description: Option<String>,
labels: Vec<Label>,
assignees: Vec<Did>,
verbose: bool,
quiet: bool,
- cache: &mut issue::Cache<'_, Repo, WriteAs<'_, Signer>, cob::cache::StoreWriter>,
+ cache: &mut issue::Cache<
+ '_,
+ impl WriteRepository + cob::Store<Namespace = NodeId>,
+ WriteAs<'_, impl crypto::Signer>,
+ cob::cache::StoreWriter,
+ >,
profile: &Profile,
-) -> anyhow::Result<()>
-where
- Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<()> {
let (title, description) = if let (Some(t), Some(d)) = (title.as_ref(), description.as_ref()) {
(t.to_owned(), d.to_owned())
} else if let Some((t, d)) = term::issue::get_title_description(title, description)? {
@@ -401,10 +399,7 @@ fn edit<'a, 'b, 'g, Repo, Signer>(
) -> anyhow::Result<issue::IssueMut<'a, 'b, 'g, Repo, Signer, cob::cache::StoreWriter>>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let id = id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&id)?;
diff --git a/crates/radicle-cli/src/commands/issue/comment.rs b/crates/radicle-cli/src/commands/issue/comment.rs
index 03111848b..07599af80 100644
--- a/crates/radicle-cli/src/commands/issue/comment.rs
+++ b/crates/radicle-cli/src/commands/issue/comment.rs
@@ -1,6 +1,7 @@
use radicle::Profile;
use radicle::cob::store::access::WriteAs;
use radicle::cob::thread;
+use radicle::crypto;
use radicle::storage::WriteRepository;
use radicle::{cob, git, issue, storage};
@@ -9,26 +10,20 @@ use crate::terminal as term;
use crate::terminal::Element as _;
use crate::terminal::patch::Message;
-pub(super) fn comment<Signer>(
+pub(super) fn comment(
profile: &Profile,
repo: &storage::git::Repository,
issues: &mut issue::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::Store<cob::cache::Write>,
>,
id: Rev,
message: Message,
reply_to: Option<Rev>,
quiet: bool,
-) -> Result<(), anyhow::Error>
-where
- Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>,
-{
+) -> Result<(), anyhow::Error> {
let reply_to = reply_to
.map(|rev| rev.resolve::<git::Oid>(repo.raw()))
.transpose()?;
@@ -47,26 +42,20 @@ where
Ok(())
}
-pub(super) fn edit<Signer>(
+pub(super) fn edit(
profile: &Profile,
repo: &storage::git::Repository,
issues: &mut issue::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::Store<cob::cache::Write>,
>,
id: Rev,
message: Message,
comment_id: Rev,
quiet: bool,
-) -> Result<(), anyhow::Error>
-where
- Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>,
-{
+) -> Result<(), anyhow::Error> {
let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
let comment_id = comment_id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&issue_id)?;
diff --git a/crates/radicle-cli/src/commands/patch/edit.rs b/crates/radicle-cli/src/commands/patch/edit.rs
index 5f495195d..1d281367d 100644
--- a/crates/radicle-cli/src/commands/patch/edit.rs
+++ b/crates/radicle-cli/src/commands/patch/edit.rs
@@ -29,17 +29,18 @@ pub fn run(
}
}
-fn edit_root<Signer>(
- mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>,
+fn edit_root(
+ mut patch: patch::PatchMut<
+ '_,
+ '_,
+ '_,
+ Repository,
+ impl crypto::Signer,
+ cob::cache::StoreWriter,
+ >,
title: Title,
description: String,
-) -> anyhow::Result<()>
-where
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<()> {
let title = if title.as_ref() != patch.title() {
Some(title)
} else {
@@ -73,18 +74,19 @@ where
Ok(())
}
-fn edit_revision<Signer>(
- mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>,
+fn edit_revision(
+ mut patch: patch::PatchMut<
+ '_,
+ '_,
+ '_,
+ Repository,
+ impl crypto::Signer,
+ cob::cache::StoreWriter,
+ >,
revision: patch::RevisionId,
title: Title,
description: String,
-) -> anyhow::Result<()>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<()> {
let embeds = patch.embeds().to_owned();
let mut message = title.to_string();
let message = if description.is_empty() {
diff --git a/crates/radicle-cli/src/commands/patch/review/builder.rs b/crates/radicle-cli/src/commands/patch/review/builder.rs
index bd4f0e520..09f7e4150 100644
--- a/crates/radicle-cli/src/commands/patch/review/builder.rs
+++ b/crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -24,7 +24,6 @@ use radicle::cob::{CodeLocation, CodeRange};
use radicle::crypto;
use radicle::git;
use radicle::git::Oid;
-use radicle::node::device::Device;
use radicle::prelude::*;
use radicle::storage::git::{Repository, cob::DraftStore};
use radicle_surf::diff::*;
@@ -614,15 +613,12 @@ impl<'a> ReviewBuilder<'a> {
}
/// Run the review builder for the given revision.
- pub fn run<G>(
+ pub fn run(
self,
revision: &Revision,
opts: &mut git::raw::DiffOptions,
- signer: &Device<G>,
- ) -> anyhow::Result<()>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> anyhow::Result<()> {
let repo = self.repo.raw();
let base = repo.find_commit((*revision.base()).into())?;
let patch_id = self.patch_id;
diff --git a/crates/radicle-cli/src/commands/remote/add.rs b/crates/radicle-cli/src/commands/remote/add.rs
index f4f246098..0a7f27f5e 100644
--- a/crates/radicle-cli/src/commands/remote/add.rs
+++ b/crates/radicle-cli/src/commands/remote/add.rs
@@ -4,7 +4,6 @@ use radicle::Profile;
use radicle::git;
use radicle::git::fmt::RefString;
use radicle::prelude::*;
-use radicle_crypto::PublicKey;
use crate::commands::checkout;
use crate::commands::follow;
diff --git a/crates/radicle-cli/src/commands/sync.rs b/crates/radicle-cli/src/commands/sync.rs
index 4b40bb4fb..976729819 100644
--- a/crates/radicle-cli/src/commands/sync.rs
+++ b/crates/radicle-cli/src/commands/sync.rs
@@ -196,7 +196,7 @@ fn announce_refs(
"nothing to announce, repository {rid} is not available locally"
));
};
- if let Err(e) = repo.remote(&profile.public_key) {
+ if let Err(e) = repo.remote(profile.id()) {
if e.is_not_found() {
term::println(term::format::italic(
"Nothing to announce, you don't have a fork of this repository.",
diff --git a/crates/radicle-cli/src/commands/watch.rs b/crates/radicle-cli/src/commands/watch.rs
index b0c73a206..4fa4c878a 100644
--- a/crates/radicle-cli/src/commands/watch.rs
+++ b/crates/radicle-cli/src/commands/watch.rs
@@ -21,7 +21,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
.refstr
.qualified()
.ok_or_else(|| anyhow!("reference must be fully-qualified, eg. 'refs/heads/master'"))?;
- let nid = args.node.unwrap_or(profile.public_key);
+ let nid = args.node.unwrap_or(*profile.id());
let (_, rid) = rid_or_cwd(args.repo)?;
let repo = storage.repository(rid)?;
let now = time::SystemTime::now();
diff --git a/crates/radicle-cli/src/terminal/cob.rs b/crates/radicle-cli/src/terminal/cob.rs
index c8fa266a1..85887e28e 100644
--- a/crates/radicle-cli/src/terminal/cob.rs
+++ b/crates/radicle-cli/src/terminal/cob.rs
@@ -5,6 +5,7 @@ use radicle::{
cache::{MigrateCallback, MigrateProgress},
store::access::{ReadOnly, WriteAs},
},
+ crypto,
prelude::NodeId,
profile,
storage::ReadRepository,
@@ -72,7 +73,7 @@ where
/// Return a read-write handle for the patches cache.
/// Prefer this over [`radicle::profile::Home::patches_mut`],
/// to obtain an error hint in case migrations must be run.
-pub fn patches_mut<'a, 'b, Repo, Signer>(
+pub fn patches_mut<'a, 'b, Repo, Signer: crypto::Signer>(
profile: &Profile,
repository: &'a Repo,
signer: &'b Signer,
@@ -97,7 +98,7 @@ where
/// Return a read-write handle for the issues cache.
/// Prefer this over [`radicle::profile::Home::issues_mut`],
/// to obtain an error hint in case migrations must be run.
-pub fn issues_mut<'a, 'b, Repo, Signer>(
+pub fn issues_mut<'a, 'b, Repo, Signer: crypto::Signer>(
profile: &Profile,
repository: &'a Repo,
signer: &'b Signer,
diff --git a/crates/radicle-cli/src/terminal/comment.rs b/crates/radicle-cli/src/terminal/comment.rs
index b5d0dd1c7..8791a2795 100644
--- a/crates/radicle-cli/src/terminal/comment.rs
+++ b/crates/radicle-cli/src/terminal/comment.rs
@@ -11,7 +11,7 @@ pub fn header<T>(
profile: &Profile,
) -> term::hstack::HStack<'static> {
let author = comment.author();
- let author = Author::new(&author, profile, false);
+ let author = Author::new(author, profile, false);
let (alias, nid) = author.labels();
term::hstack::HStack::default()
diff --git a/crates/radicle-cli/src/terminal/io.rs b/crates/radicle-cli/src/terminal/io.rs
index cfc26a083..6a8998426 100644
--- a/crates/radicle-cli/src/terminal/io.rs
+++ b/crates/radicle-cli/src/terminal/io.rs
@@ -2,11 +2,10 @@ use anyhow::anyhow;
use radicle::cob::Reaction;
use radicle::cob::issue::Issue;
use radicle::cob::thread::{Comment, CommentId};
+use radicle::crypto::SigningKey;
use radicle::crypto::ssh::Keystore;
-use radicle::crypto::ssh::keystore::MemorySigner;
-use radicle::node::device::{BoxedDevice, Device};
-use radicle::profile::Profile;
use radicle::profile::env::RAD_PASSPHRASE;
+use radicle::profile::{Profile, Signer, SignerError};
pub use radicle_term::io::*;
pub use radicle_term::spinner;
@@ -45,14 +44,34 @@ impl inquire::validator::StringValidator for PassphraseValidator {
/// Get the signer. First we try getting it from ssh-agent; otherwise, we prompt the user,
/// if we're connected to a TTY.
-pub fn signer(profile: &Profile) -> anyhow::Result<BoxedDevice> {
- match profile.signer() {
+pub fn signer(profile: &Profile) -> anyhow::Result<Signer> {
+ let err = match profile.signer() {
Ok(signer) => return Ok(signer),
- Err(err) if !err.prompt_for_passphrase() => return Err(anyhow!(err)),
- Err(_) => {
- // The error returned is potentially recoverable by prompting
- // the user for the correct passphrase.
+ Err(err) => err,
+ };
+
+ match err {
+ SignerError::LoadError(radicle::crypto::LoadError::InvalidPassphrase) => {
+ super::warning(format!(
+ "The passphrase for your Radicle key provided in the environment variable `{RAD_PASSPHRASE}` is invalid. Please try again."
+ ));
+ }
+ SignerError::AgentConnection(err) => {
+ super::warning(format!(
+ "Failed to connect to ssh-agent: {err}. Falling back to passphrase prompt."
+ ));
+ }
+ SignerError::Agent(radicle::crypto::ssh::agent::IntoSignerError::IdentityNotFound {
+ identity,
+ }) => {
+ super::warning(format!(
+ "The Radicle key for `{identity}` is not registered with ssh-agent. Please run `rad auth` to register it."
+ ));
}
+ err @ SignerError::LoadError(_)
+ | err @ SignerError::InvalidPublicKey(_)
+ | err @ SignerError::Agent(_)
+ | err @ SignerError::Keystore(_) => return Err(anyhow!(err)),
}
let validator = PassphraseValidator::new(profile.keystore.clone());
@@ -65,11 +84,11 @@ pub fn signer(profile: &Profile) -> anyhow::Result<BoxedDevice> {
}
};
let spinner = spinner("Unsealing key…");
- let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
+ let signer = SigningKey::load(&profile.keystore, Some(passphrase))?;
spinner.finish();
- Ok(Device::from(signer).boxed())
+ Ok(Signer::Key(signer))
}
pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
diff --git a/crates/radicle-cli/tests/commands/clone.rs b/crates/radicle-cli/tests/commands/clone.rs
index 8ccee2b80..521a6a779 100644
--- a/crates/radicle-cli/tests/commands/clone.rs
+++ b/crates/radicle-cli/tests/commands/clone.rs
@@ -119,7 +119,7 @@ fn rad_clone_partial_fail() {
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let mut eve = environment.node("eve");
- let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT").unwrap();
+ let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT").unwrap();
// Set up a test project.
let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
diff --git a/crates/radicle-cli/tests/util/environment.rs b/crates/radicle-cli/tests/util/environment.rs
index f42895b09..3494bf332 100644
--- a/crates/radicle-cli/tests/util/environment.rs
+++ b/crates/radicle-cli/tests/util/environment.rs
@@ -2,8 +2,8 @@ use std::path::PathBuf;
use std::str::FromStr;
use radicle::cob::cache::COBS_DB_FILE;
-use radicle::crypto::ssh::{Keystore, keystore::MemorySigner};
-use radicle::crypto::{KeyPair, Seed};
+use radicle::crypto::ssh::Keystore;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::git;
use radicle::node;
use radicle::node::policy::store as policy;
@@ -136,7 +136,9 @@ impl Environment {
pub fn profile_with(&mut self, config: profile::Config) -> Profile {
let alias = config.alias().clone();
let home = self.rad_home(&alias);
- let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
+ let seed = radicle::crypto::Seed::new([!(self.users as u8); 32]);
+ let keypair = SigningKey::from_seed(seed);
+ let public_key = keypair.public_key();
let policies_db = home.node().join(POLICIES_DB_FILE);
let cobs_db = home.cobs().join(COBS_DB_FILE);
@@ -146,7 +148,7 @@ impl Environment {
home.storage(),
radicle::git::UserInfo {
alias: alias.clone(),
- key: keypair.pk.into(),
+ key: *public_key,
},
)
.unwrap();
@@ -158,7 +160,7 @@ impl Environment {
home.database_mut(node::db::config::Config::default())
.unwrap()
.init(
- &keypair.pk.into(),
+ public_key,
config.node.features(),
&alias,
&config.node.user_agent(),
@@ -171,7 +173,7 @@ impl Environment {
transport::local::register(storage.clone());
let keystore = Keystore::new(&home.keys());
- keystore.store(keypair.clone(), "radicle", None).unwrap();
+ keystore.store(&keypair, "radicle", None).unwrap();
// Ensures that each user has a unique but deterministic public key.
self.users += 1;
@@ -180,7 +182,7 @@ impl Environment {
home,
storage,
keystore,
- public_key: keypair.pk.into(),
+ public_key: *public_key,
config,
}
}
@@ -197,7 +199,7 @@ impl Environment {
/// as for each of them a convenience function
/// (resp. [`Environment::node`], [`Environment::relay`], [`Environment::seed`]).
/// is provided to reduce boilerplate.
- pub fn node_with(&mut self, node: Config) -> Node<MemorySigner> {
+ pub fn node_with(&mut self, node: Config) -> Node {
let alias = node.alias.clone();
let profile = self.profile_with(profile::Config {
node,
@@ -206,21 +208,21 @@ impl Environment {
Node::new(profile)
}
- /// Convenience method for creating a [`Node<MemorySigner>`]
+ /// Convenience method for creating a [`Node`]
/// using configuration [`config::node`] within this [`Environment`].
- pub fn node(&mut self, alias: &'static str) -> Node<MemorySigner> {
+ pub fn node(&mut self, alias: &'static str) -> Node {
self.node_with(config::node(alias))
}
- /// Convenience method for creating a [`Node<MemorySigner>`]
+ /// Convenience method for creating a [`Node`]
/// using configuration [`config::relay`] within this [`Environment`].
- pub fn relay(&mut self, alias: &'static str) -> Node<MemorySigner> {
+ pub fn relay(&mut self, alias: &'static str) -> Node {
self.node_with(config::relay(alias))
}
- /// Convenience method for creating a [`Node<MemorySigner>`]
+ /// Convenience method for creating a [`Node`]
/// using configuration [`config::seed`] within this [`Environment`].
- pub fn seed(&mut self, alias: &'static str) -> Node<MemorySigner> {
+ pub fn seed(&mut self, alias: &'static str) -> Node {
self.node_with(config::seed(alias))
}
@@ -293,7 +295,7 @@ impl HasAlias for Alias {
}
}
-impl HasAlias for Node<MemorySigner> {
+impl HasAlias for Node {
fn alias(&self) -> &Alias {
&self.config.alias
}
@@ -305,7 +307,7 @@ impl HasAlias for Profile {
}
}
-impl<G> HasAlias for NodeHandle<G> {
+impl HasAlias for NodeHandle {
fn alias(&self) -> &Alias {
&self.alias
}
diff --git a/crates/radicle-cob/Cargo.toml b/crates/radicle-cob/Cargo.toml
index 89fe314aa..2eff83f36 100644
--- a/crates/radicle-cob/Cargo.toml
+++ b/crates/radicle-cob/Cargo.toml
@@ -31,7 +31,6 @@ radicle-git-metadata = { workspace = true }
radicle-oid = { workspace = true, features = ["git2", "serde", "sha1", "std"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
-signature = { workspace = true }
thiserror = { workspace = true, default-features = true }
[dev-dependencies]
diff --git a/crates/radicle-cob/src/backend/git/change.rs b/crates/radicle-cob/src/backend/git/change.rs
index 836e07573..5694456f8 100644
--- a/crates/radicle-cob/src/backend/git/change.rs
+++ b/crates/radicle-cob/src/backend/git/change.rs
@@ -94,18 +94,17 @@ impl change::Storage for git2::Repository {
type ObjectId = Oid;
type Parent = Oid;
- type Signatures = ExtendedSignature;
- fn store<Signer>(
+ type PublicKey = crypto::PublicKey;
+ type Signature = crypto::Signature;
+
+ fn store(
&self,
resource: Option<Self::Parent>,
mut related: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: store::Template<Self::ObjectId>,
- ) -> Result<Entry, Self::StoreError>
- where
- Signer: signature::Signer<Self::Signatures>,
- {
+ ) -> Result<Entry, Self::StoreError> {
let change::Template {
type_name,
tips,
@@ -116,7 +115,8 @@ impl change::Storage for git2::Repository {
let manifest = store::Manifest::new(type_name, Version::default());
let revision = write_manifest(self, &manifest, embeds, &contents)?;
let tree = self.find_tree(revision)?;
- let signature = signer.sign(revision.as_bytes());
+ let signature = ExtendedSignature::try_sign(signer, revision.as_bytes())
+ .map_err(|source| error::Create::Signer(Box::new(source)))?;
// Make sure there are no duplicates in the related list.
related.sort();
diff --git a/crates/radicle-cob/src/change.rs b/crates/radicle-cob/src/change.rs
index 70b23bdb8..c03848793 100644
--- a/crates/radicle-cob/src/change.rs
+++ b/crates/radicle-cob/src/change.rs
@@ -8,4 +8,4 @@ pub use store::{Contents, EntryId, Storage, Template, Timestamp};
use crate::signatures::ExtendedSignature;
/// A single change in the change graph.
-pub type Entry = store::Entry<Oid, Oid, ExtendedSignature>;
+pub type Entry = store::Entry<Oid, Oid, ExtendedSignature<crypto::PublicKey>>;
diff --git a/crates/radicle-cob/src/change/store.rs b/crates/radicle-cob/src/change/store.rs
index b4e9485cd..7a671ffc9 100644
--- a/crates/radicle-cob/src/change/store.rs
+++ b/crates/radicle-cob/src/change/store.rs
@@ -2,6 +2,8 @@
use std::{error::Error, fmt, num::NonZeroUsize};
+use crypto::signature::Verifier as _;
+use crypto::{ExtendedSignature, VerifyingKey};
use nonempty::NonEmpty;
use oid::Oid;
use serde::{Deserialize, Serialize};
@@ -16,26 +18,32 @@ pub trait Storage {
type ObjectId;
type Parent;
- type Signatures;
+
+ type PublicKey;
+ type Signature;
/// Store a new change entry.
#[allow(clippy::type_complexity)]
- fn store<G>(
+ fn store(
&self,
resource: Option<Self::Parent>,
related: Vec<Self::Parent>,
- signer: &G,
+ signer: &impl crypto::Signer,
template: Template<Self::ObjectId>,
- ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::StoreError>
- where
- G: signature::Signer<Self::Signatures>;
+ ) -> Result<
+ Entry<Self::Parent, Self::ObjectId, ExtendedSignature<Self::PublicKey, Self::Signature>>,
+ Self::StoreError,
+ >;
/// Load a change entry.
#[allow(clippy::type_complexity)]
fn load(
&self,
id: Self::ObjectId,
- ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::LoadError>;
+ ) -> Result<
+ Entry<Self::Parent, Self::ObjectId, ExtendedSignature<Self::PublicKey, Self::Signature>>,
+ Self::LoadError,
+ >;
/// Returns the parents of the object with the specified ID.
fn parents_of(&self, id: &Oid) -> Result<Vec<Oid>, Self::LoadError>;
@@ -120,22 +128,28 @@ where
Id: AsRef<[u8]>,
{
pub fn valid_signatures(&self) -> bool {
- self.signature
- .iter()
- .all(|(key, sig)| key.verify(self.revision.as_ref(), sig).is_ok())
+ use crypto::signature::Verifier as _;
+
+ self.signature.iter().all(|(key, sig)| {
+ VerifyingKey::try_from(key)
+ .and_then(|verifier| verifier.verify(self.revision.as_ref(), sig))
+ .is_ok()
+ })
}
}
-impl<R, Id> Entry<R, Id, signatures::ExtendedSignature>
+impl<R, Id> Entry<R, Id, signatures::ExtendedSignature<crypto::PublicKey>>
where
Id: AsRef<[u8]>,
{
pub fn valid_signatures(&self) -> bool {
- self.signature.verify(self.revision.as_ref())
+ VerifyingKey::try_from(self.signature.key())
+ .and_then(|verifier| verifier.verify(self.revision.as_ref(), self.signature.sig()))
+ .is_ok()
}
pub fn author(&self) -> &crypto::PublicKey {
- &self.signature.key
+ self.signature.key()
}
}
diff --git a/crates/radicle-cob/src/change_graph.rs b/crates/radicle-cob/src/change_graph.rs
index b7923e434..910942bf4 100644
--- a/crates/radicle-cob/src/change_graph.rs
+++ b/crates/radicle-cob/src/change_graph.rs
@@ -8,7 +8,7 @@ use oid::Oid;
use crate::{
CollaborativeObject, Entry, EntryId, History, ObjectId, TypeName, change, object,
- object::collaboration::Evaluate, signatures::ExtendedSignature,
+ object::collaboration::Evaluate,
};
#[derive(Debug, thiserror::Error)]
@@ -37,7 +37,12 @@ impl ChangeGraph {
oid: &ObjectId,
) -> Option<ChangeGraph>
where
- S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
+ S: change::Storage<
+ ObjectId = Oid,
+ Parent = Oid,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
+ >,
{
let tip_refs = tip_refs.collect::<Vec<_>>();
diff --git a/crates/radicle-cob/src/lib.rs b/crates/radicle-cob/src/lib.rs
index 90d8ff3bd..1b34e7f7e 100644
--- a/crates/radicle-cob/src/lib.rs
+++ b/crates/radicle-cob/src/lib.rs
@@ -84,7 +84,6 @@ pub mod history;
pub use history::History;
pub mod signatures;
-use signatures::ExtendedSignature;
pub mod type_name;
pub use type_name::TypeName;
@@ -116,6 +115,11 @@ mod tests;
pub trait Store
where
Self: object::Storage
- + change::Storage<ObjectId = oid::Oid, Parent = oid::Oid, Signatures = ExtendedSignature>,
+ + change::Storage<
+ ObjectId = oid::Oid,
+ Parent = oid::Oid,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
+ >,
{
}
diff --git a/crates/radicle-cob/src/object/collaboration/create.rs b/crates/radicle-cob/src/object/collaboration/create.rs
index 0ab18851d..31c1fb011 100644
--- a/crates/radicle-cob/src/object/collaboration/create.rs
+++ b/crates/radicle-cob/src/object/collaboration/create.rs
@@ -51,23 +51,23 @@ impl Create {
///
/// The `args` are the metadata for this [`CollaborativeObject`]. See
/// [`Create`] for further information.
-pub fn create<T, S, G>(
- storage: &S,
- signer: &G,
+pub fn create<T, Storage>(
+ storage: &Storage,
+ signer: &impl crypto::Signer,
resource: Option<Oid>,
related: Vec<Oid>,
- identifier: &<S as crate::object::Storage>::Namespace,
+ identifier: &<Storage as crate::object::Storage>::Namespace,
args: Create,
) -> Result<CollaborativeObject<T>, error::Create>
where
- T: Evaluate<S>,
- S: crate::object::Storage
- + crate::change::Storage<
+ T: Evaluate<Storage>,
+ Storage: crate::object::Storage,
+ Storage: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = crate::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
- G: signature::Signer<crate::ExtendedSignature>,
{
let type_name = args.type_name.clone();
let version = args.version;
diff --git a/crates/radicle-cob/src/object/collaboration/get.rs b/crates/radicle-cob/src/object/collaboration/get.rs
index 66bce507e..235a950d5 100644
--- a/crates/radicle-cob/src/object/collaboration/get.rs
+++ b/crates/radicle-cob/src/object/collaboration/get.rs
@@ -1,7 +1,5 @@
// Copyright © 2022 The Radicle Link Contributors
-use crypto::ssh::ExtendedSignature;
-
use crate::{CollaborativeObject, Evaluate, ObjectId, TypeName, change_graph::ChangeGraph};
use super::error;
@@ -26,7 +24,8 @@ where
S: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
let tip_refs = storage
diff --git a/crates/radicle-cob/src/object/collaboration/info.rs b/crates/radicle-cob/src/object/collaboration/info.rs
index 89aef0272..d96596a82 100644
--- a/crates/radicle-cob/src/object/collaboration/info.rs
+++ b/crates/radicle-cob/src/object/collaboration/info.rs
@@ -6,7 +6,6 @@
use std::collections::BTreeSet;
-use crypto::ssh::ExtendedSignature;
use oid::Oid;
use crate::{ObjectId, TypeName, change_graph::ChangeGraph};
@@ -43,7 +42,8 @@ where
S: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
let tip_refs = storage
diff --git a/crates/radicle-cob/src/object/collaboration/list.rs b/crates/radicle-cob/src/object/collaboration/list.rs
index 04c8b5865..22910020c 100644
--- a/crates/radicle-cob/src/object/collaboration/list.rs
+++ b/crates/radicle-cob/src/object/collaboration/list.rs
@@ -21,7 +21,8 @@ where
S: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = crate::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
let references = storage
diff --git a/crates/radicle-cob/src/object/collaboration/update.rs b/crates/radicle-cob/src/object/collaboration/update.rs
index 44bf7fe82..7b7a2ded4 100644
--- a/crates/radicle-cob/src/object/collaboration/update.rs
+++ b/crates/radicle-cob/src/object/collaboration/update.rs
@@ -5,8 +5,8 @@ use nonempty::NonEmpty;
use oid::Oid;
use crate::{
- CollaborativeObject, Embed, Evaluate, ExtendedSignature, ObjectId, TypeName, change,
- change_graph::ChangeGraph, history::EntryId,
+ CollaborativeObject, Embed, Evaluate, ObjectId, TypeName, change, change_graph::ChangeGraph,
+ history::EntryId,
};
use super::error;
@@ -55,19 +55,23 @@ pub struct Update {
///
/// The `args` are the metadata for this [`CollaborativeObject`]
/// update. See [`Update`] for further information.
-pub fn update<T, S, G>(
- storage: &S,
- signer: &G,
+pub fn update<T, Storage>(
+ storage: &Storage,
+ signer: &impl crypto::Signer,
resource: Option<Oid>,
related: Vec<Oid>,
- identifier: &S::Namespace,
+ identifier: &Storage::Namespace,
args: Update,
) -> Result<Updated<T>, error::Update>
where
- T: Evaluate<S>,
- S: crate::object::Storage,
- S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
- G: signature::Signer<ExtendedSignature>,
+ T: Evaluate<Storage>,
+ Storage: crate::object::Storage,
+ Storage: change::Storage<
+ ObjectId = Oid,
+ Parent = Oid,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
+ >,
{
let Update {
type_name: ref typename,
diff --git a/crates/radicle-cob/src/signatures.rs b/crates/radicle-cob/src/signatures.rs
index febdd8d90..137a6f59d 100644
--- a/crates/radicle-cob/src/signatures.rs
+++ b/crates/radicle-cob/src/signatures.rs
@@ -7,22 +7,23 @@ use std::{
ops::{Deref, DerefMut},
};
-use crypto::{PublicKey, ssh};
+use crypto::PublicKey;
use metadata::commit::{
CommitData,
headers::Signature::{Pgp, Ssh},
};
-pub use ssh::ExtendedSignature;
+pub use crypto::ExtendedSignature;
+use crypto::Signature;
pub mod error;
// FIXME(kim): This should really be a HashMap with a no-op Hasher -- PublicKey
// collisions are catastrophic
-#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct Signatures(BTreeMap<PublicKey, crypto::Signature>);
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Signatures(BTreeMap<PublicKey, Signature>);
impl Deref for Signatures {
- type Target = BTreeMap<PublicKey, crypto::Signature>;
+ type Target = BTreeMap<PublicKey, Signature>;
fn deref(&self) -> &Self::Target {
&self.0
@@ -36,20 +37,18 @@ impl DerefMut for Signatures {
}
impl From<ExtendedSignature> for Signatures {
- fn from(ExtendedSignature { key, sig }: ExtendedSignature) -> Self {
- let mut map = BTreeMap::new();
- map.insert(key, sig);
- map.into()
+ fn from(signature: ExtendedSignature) -> Self {
+ Self([signature.into_pair()].into())
}
}
-impl From<BTreeMap<PublicKey, crypto::Signature>> for Signatures {
- fn from(map: BTreeMap<PublicKey, crypto::Signature>) -> Self {
+impl From<BTreeMap<PublicKey, Signature>> for Signatures {
+ fn from(map: BTreeMap<PublicKey, Signature>) -> Self {
Self(map)
}
}
-impl From<Signatures> for BTreeMap<PublicKey, crypto::Signature> {
+impl From<Signatures> for BTreeMap<PublicKey, Signature> {
fn from(s: Signatures) -> Self {
s.0
}
@@ -67,27 +66,27 @@ impl<Tree, Parent> TryFrom<&CommitData<Tree, Parent>> for Signatures {
Pgp(_) => None,
Ssh(pem) => Some(
ExtendedSignature::from_pem(pem.as_bytes())
- .map_err(error::Signatures::from),
+ .map_err(error::Signatures::from)
+ .map(ExtendedSignature::into_pair),
),
}
})
- .map(|r| r.map(|es| (es.key, es.sig)))
.collect::<Result<_, _>>()
}
}
-impl FromIterator<(PublicKey, crypto::Signature)> for Signatures {
+impl FromIterator<(PublicKey, Signature)> for Signatures {
fn from_iter<T>(iter: T) -> Self
where
- T: IntoIterator<Item = (PublicKey, crypto::Signature)>,
+ T: IntoIterator<Item = (PublicKey, Signature)>,
{
Self(BTreeMap::from_iter(iter))
}
}
impl IntoIterator for Signatures {
- type Item = (PublicKey, crypto::Signature);
- type IntoIter = <BTreeMap<PublicKey, crypto::Signature> as IntoIterator>::IntoIter;
+ type Item = (PublicKey, Signature);
+ type IntoIter = <BTreeMap<PublicKey, Signature> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
@@ -99,19 +98,15 @@ impl Extend<ExtendedSignature> for Signatures {
where
T: IntoIterator<Item = ExtendedSignature>,
{
- for ExtendedSignature { key, sig } in iter {
- self.insert(key, sig);
- }
+ self.extend(iter.into_iter().map(ExtendedSignature::into_pair))
}
}
-impl Extend<(PublicKey, crypto::Signature)> for Signatures {
+impl Extend<(PublicKey, Signature)> for Signatures {
fn extend<T>(&mut self, iter: T)
where
- T: IntoIterator<Item = (PublicKey, crypto::Signature)>,
+ T: IntoIterator<Item = (PublicKey, Signature)>,
{
- for (key, sig) in iter {
- self.insert(key, sig);
- }
+ self.0.extend(iter)
}
}
diff --git a/crates/radicle-cob/src/signatures/error.rs b/crates/radicle-cob/src/signatures/error.rs
index e10e8e66c..a1f9793f5 100644
--- a/crates/radicle-cob/src/signatures/error.rs
+++ b/crates/radicle-cob/src/signatures/error.rs
@@ -1,6 +1,6 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
-use crypto::ssh::ExtendedSignatureError;
+use crypto::ssh::ExtendedSignaturePemError;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -17,7 +17,7 @@ pub enum Signature {
#[non_exhaustive]
pub enum Signatures {
#[error(transparent)]
- ExtendedSignature(#[from] ExtendedSignatureError),
+ ExtendedSignature(#[from] ExtendedSignaturePemError),
#[error(transparent)]
Signature(#[from] Signature),
diff --git a/crates/radicle-cob/src/test/storage.rs b/crates/radicle-cob/src/test/storage.rs
index 2df1c24c6..ae5e35ec8 100644
--- a/crates/radicle-cob/src/test/storage.rs
+++ b/crates/radicle-cob/src/test/storage.rs
@@ -8,7 +8,6 @@ use tempfile::TempDir;
use crate::{
ObjectId, Store, change,
object::{self, Reference},
- signatures,
};
pub mod error {
@@ -69,29 +68,38 @@ impl change::Storage for Storage {
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
type Parent = <git2::Repository as change::Storage>::Parent;
- type Signatures = <git2::Repository as change::Storage>::Signatures;
- fn store<Signer>(
+ type PublicKey = <git2::Repository as change::Storage>::PublicKey;
+ type Signature = <git2::Repository as change::Storage>::Signature;
+
+ fn store(
&self,
authority: Option<Self::Parent>,
parents: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: change::Template<Self::ObjectId>,
) -> Result<
- change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>,
+ change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ crypto::ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
Self::StoreError,
- >
- where
- Signer: signature::Signer<signatures::ExtendedSignature>,
- {
+ > {
self.as_raw().store(authority, parents, signer, spec)
}
fn load(
&self,
id: Self::ObjectId,
- ) -> Result<change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::LoadError>
- {
+ ) -> Result<
+ change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ crypto::ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
+ Self::LoadError,
+ > {
self.as_raw().load(id)
}
diff --git a/crates/radicle-cob/src/tests.rs b/crates/radicle-cob/src/tests.rs
index 48a0d2e54..79a747029 100644
--- a/crates/radicle-cob/src/tests.rs
+++ b/crates/radicle-cob/src/tests.rs
@@ -8,8 +8,10 @@ use crate::{ObjectId, TypeName, object, test::arbitrary::Invalid};
mod git {
use std::ops::ControlFlow;
- use crypto::test::signer::MockSigner;
- use crypto::{PublicKey, Signer};
+ use crypto::PublicKey;
+ use crypto::Signer as _;
+ use crypto::SigningKey;
+
use nonempty::{NonEmpty, nonempty};
use qcheck::Arbitrary;
@@ -22,15 +24,16 @@ mod git {
#[test]
fn roundtrip() {
let storage = test::Storage::new();
- let signer = r#gen::<MockSigner>(1);
- let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
- let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+ let signer = r#gen::<SigningKey>(1);
+ let terry = test::Person::new(&storage, "terry", signer.public_key().to_owned()).unwrap();
+ let proj =
+ test::Project::new(&storage, "discworld", signer.public_key().to_owned()).unwrap();
let proj = test::RemoteProject {
project: proj,
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let cob = create::<NonEmpty<Entry>, _, _>(
+ let cob = create::<NonEmpty<Entry>, _>(
&storage,
&signer,
Some(proj.project.content_id),
@@ -56,7 +59,7 @@ mod git {
#[test]
fn list_cobs() {
let storage = test::Storage::new();
- let signer = r#gen::<MockSigner>(1);
+ let signer = r#gen::<SigningKey>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
@@ -64,7 +67,7 @@ mod git {
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let issue_1 = create::<NonEmpty<Entry>, _, _>(
+ let issue_1 = create::<NonEmpty<Entry>, _>(
&storage,
&signer,
Some(proj.project.content_id),
@@ -108,7 +111,7 @@ mod git {
#[test]
fn update_cob() {
let storage = test::Storage::new();
- let signer = r#gen::<MockSigner>(1);
+ let signer = r#gen::<SigningKey>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
@@ -116,7 +119,7 @@ mod git {
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let cob = create::<NonEmpty<Entry>, _, _>(
+ let cob = create::<NonEmpty<Entry>, _>(
&storage,
&signer,
Some(proj.project.content_id),
@@ -163,9 +166,9 @@ mod git {
#[test]
fn traverse_cobs() {
let storage = test::Storage::new();
- let neil_signer = r#gen::<MockSigner>(2);
+ let neil_signer = r#gen::<SigningKey>(2);
let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
- let terry_signer = r#gen::<MockSigner>(1);
+ let terry_signer = r#gen::<SigningKey>(1);
let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
let terry_proj = test::RemoteProject {
@@ -177,7 +180,7 @@ mod git {
person: neil,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let cob = create::<NonEmpty<Entry>, _, _>(
+ let cob = create::<NonEmpty<Entry>, _>(
&storage,
&terry_signer,
Some(terry_proj.project.content_id),
@@ -201,7 +204,7 @@ mod git {
)
.unwrap();
- let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
+ let Updated { object, .. } = update::<NonEmpty<Entry>, _>(
&storage,
&neil_signer,
Some(neil_proj.project.content_id),
diff --git a/crates/radicle-core/Cargo.toml b/crates/radicle-core/Cargo.toml
index e47bdfabf..75bbe04bf 100644
--- a/crates/radicle-core/Cargo.toml
+++ b/crates/radicle-core/Cargo.toml
@@ -14,13 +14,13 @@ rust-version.workspace = true
default = ["std"]
git2 = ["dep:git2", "radicle-oid/git2"]
gix = ["dep:gix-hash", "radicle-oid/gix"]
-std = ["radicle-oid/std", "thiserror/std", "schemars/std", "serde/std"]
+std = [ "multibase/std", "radicle-oid/std", "thiserror/std", "schemars/std", "serde/std"]
qcheck = ["dep:qcheck", "radicle-oid/qcheck"]
[dependencies]
git2 = { workspace = true, optional = true }
gix-hash = { workspace = true, optional = true }
-multibase = { workspace = true }
+multibase = { workspace = true, default-features = false }
proptest = { workspace = true, optional = true }
qcheck = { workspace = true, optional = true }
radicle-crypto = { workspace = true }
diff --git a/crates/radicle-core/src/repo.rs b/crates/radicle-core/src/repo.rs
index 7e9bb1c77..92a700a1a 100644
--- a/crates/radicle-core/src/repo.rs
+++ b/crates/radicle-core/src/repo.rs
@@ -12,8 +12,8 @@ pub const RAD_PREFIX: &str = "rad:";
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum IdError {
- #[error(transparent)]
- Multibase(#[from] multibase::Error),
+ #[error("invalid multibase string: {0}")]
+ Multibase(#[cfg_attr(feature = "std", source)] multibase::Error),
#[error("invalid length: expected {} bytes, got {actual} bytes", Oid::LEN_SHA1)]
Length { actual: usize },
#[error(fmt = fmt_mismatched_base_encoding)]
@@ -106,7 +106,7 @@ impl RepoId {
///
/// [multibase]: https://github.com/multiformats/multibase?tab=readme-ov-file#multibase-table
pub fn from_canonical(input: &str) -> Result<Self, IdError> {
- let (base, bytes) = multibase::decode(input)?;
+ let (base, bytes) = multibase::decode(input).map_err(IdError::Multibase)?;
Self::guard_base_encoding(input, base)?;
let bytes: [u8; Oid::LEN_SHA1] =
bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
diff --git a/crates/radicle-crypto/Cargo.toml b/crates/radicle-crypto/Cargo.toml
index d9b2b0b9a..bd21b141a 100644
--- a/crates/radicle-crypto/Cargo.toml
+++ b/crates/radicle-crypto/Cargo.toml
@@ -5,39 +5,44 @@ homepage.workspace = true
repository.workspace = true
license.workspace = true
version = "0.17.0"
-authors = [
- "cloudhead <cloudhead@radicle.xyz>",
- "Fintan Halpenny <fintan.halpenny@gmail.com>",
-]
edition.workspace = true
rust-version.workspace = true
[features]
-test = ["fastrand", "qcheck"]
-ssh = ["ssh-agent-lib", "ssh-key"]
+default = ["multibase", "std", "x25519"]
+alloc = ["ed25519-dalek/alloc", "curve25519-dalek/alloc", "x25519-dalek?/alloc", "zeroize/alloc"]
+cyphernet = ["dep:cyphernet", "alloc", "diffie-hellman", "multibase"]
+diffie-hellman = []
+multibase = ["dep:multibase", "cyphernet?/multibase"]
+schemars = ["dep:schemars", "serde"]
+serde = ["dep:serde", "ed25519-dalek/serde", "x25519-dalek/serde", "zeroize/serde"]
+ssh = ["ssh-agent-lib", "ssh-key", "alloc"]
+std = ["alloc", "ed25519-dalek/std", "multibase?/std", "schemars?/std", "serde?/std", "ssh-key?/std", "thiserror/std", "zeroize/std" ]
+test = ["alloc", "qcheck"]
+x25519 = ["dep:x25519-dalek"]
[dependencies]
-amplify = { workspace = true }
cyphernet = { workspace = true, features = ["ed25519"], optional = true }
-ec25519 = "0.1.0"
-fastrand = { workspace = true, optional = true }
-multibase = { workspace = true }
-qcheck = { workspace = true, optional = true }
git-ref-format-core = { workspace = true, optional = true }
-schemars = { workspace = true, optional = true, features = ["derive", "std"] }
-serde = { workspace = true, features = ["derive", "std"] }
-signature = { workspace = true, features = ["std"] }
+multibase = { workspace = true, default-features = false, optional = true }
+qcheck = { workspace = true, optional = true }
+schemars = { workspace = true, default-features = false, features = ["derive"], optional = true }
+serde = { workspace = true, default-features = false, features = ["derive"], optional = true }
sqlite = { workspace = true, features = ["bundled"], optional = true }
-ssh-agent-lib = { version = "0.6", optional = true, default-features = false, features = ["log"] }
-ssh-key = { version = "0.6.3", default-features = false, features = ["std", "encryption", "getrandom"], optional = true }
-thiserror = { workspace = true, default-features = true }
-zeroize = { workspace = true }
+ssh-agent-lib = { version = "0.6", default-features = false, features = ["log"], optional = true }
+ssh-key = { version = "0.6.3", default-features = false, features = ["ed25519", "encryption"], optional = true }
+thiserror = { workspace = true, default-features = false }
+zeroize = { workspace = true, default-features = false }
+
+# dalek
+curve25519-dalek = { version = "4", default-features = false, features = ["zeroize"] }
+ed25519-dalek = { version = "2", default-features = false, features = ["zeroize"] }
+x25519-dalek = { version = "2", default-features = false, features = ["zeroize"], optional = true }
[target.'cfg(windows)'.dependencies]
winpipe = { workspace = true }
[dev-dependencies]
-fastrand = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
tempfile = { workspace = true }
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 21769df0a..07e62489c 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -1,543 +1,819 @@
-use std::cmp::Ordering;
-use std::sync::Arc;
-use std::{fmt, ops::Deref, str::FromStr};
+#![no_std]
-use ec25519 as ed25519;
-use serde::{Deserialize, Serialize};
-use thiserror::Error;
+#[cfg(any(test, feature = "alloc"))]
+extern crate alloc;
-pub use ed25519::{Error, KeyPair, Seed, edwards25519};
+#[cfg(any(test, feature = "alloc"))]
+#[allow(unused_imports)]
+use alloc::{
+ string::{String, ToString as _},
+ vec::Vec,
+};
-pub extern crate signature;
+#[cfg(feature = "std")]
+extern crate std;
-#[cfg(feature = "ssh")]
+/// Re-exports of dalek cryptography crates (see <https://dalek.rs/>)
+/// that this crate depends on. Since they are all related to Curve25519
+/// in some way, the "25519" suffix is omitted from the name of the re-export.
+mod dalek {
+ pub extern crate curve25519_dalek as curve;
+ pub extern crate ed25519_dalek as ed;
+ #[cfg(feature = "x25519")]
+ pub extern crate x25519_dalek as x;
+}
+
+/// Re-export of the `signature` crate as re-exported
+/// by the `ed25519_dalek` crate.
+pub use dalek::ed::ed25519::signature;
+
+#[cfg(all(feature = "ssh", feature = "alloc"))]
pub mod ssh;
-#[cfg(any(test, feature = "test"))]
-pub mod test;
+
+mod seed;
+pub use seed::Seed;
/// Output of a Diffie-Hellman key exchange.
pub type SharedSecret = [u8; 32];
-/// Error returned if signing fails, eg. due to an HSM or KMS.
-#[derive(Debug, Clone, Error)]
-#[error(transparent)]
-#[non_exhaustive]
-pub struct SignerError {
- #[from]
- source: Arc<dyn std::error::Error + Send + Sync>,
+pub type Signature = dalek::ed::Signature;
+
+pub trait Signer
+where
+ Self: signature::Signer<Signature>,
+ Self: signature::Keypair<VerifyingKey = VerifyingKey>,
+ Self: AsRef<PublicKey>,
+{
+ fn public_key(&self) -> &PublicKey {
+ self.as_ref()
+ }
}
-impl SignerError {
- pub fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self {
- Self {
- source: Arc::new(source),
- }
+impl<T: ?Sized> Signer for T
+where
+ Self: signature::Signer<Signature>,
+ Self: signature::Keypair<VerifyingKey = VerifyingKey>,
+ Self: AsRef<PublicKey>,
+{
+}
+
+/// This module contains compile-time checks to ensure the following:
+/// 1. [`Signer`] is compatible with `dyn` usage.
+/// 2. [`SigningKey`] and other well-known implementations of signers
+/// implement the trait.
+///
+/// As long as this module compiles, we have reasonable confidence that we
+/// can generalize to `dyn` in the future without breaking existing code.
+///
+/// Note that this module is "dead code" in the sense that it serves no
+/// purpose at runtime, but it is useful at compile-time!
+#[allow(dead_code)]
+mod future {
+ use super::*;
+
+ /// Witnesses that [`Signer`] is `dyn`-compatible.
+ fn r#dyn(_: &dyn Signer) {}
+
+ /// Witnesses that the generic argument implements [`Signer`].
+ const fn r#impl<Witness: Signer>() {}
+
+ /// Witnesses that [`SigningKey`] implements [`Signer`].
+ const IMPL_SECRET_KEY: () = r#impl::<SigningKey>();
+
+ /// Witnesses that [`ssh::agent::AgentSigner`] implements [`Signer`].
+ #[cfg(all(feature = "ssh", feature = "std"))]
+ const IMPL_AGENT_SIGNER: () = r#impl::<ssh::agent::AgentSigner>();
+}
+
+/// Multicodec key type for Ed25519 keys.
+#[cfg(feature = "multibase")]
+pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x01];
+
+pub type PublicKeyBytes = [u8; dalek::ed::PUBLIC_KEY_LENGTH];
+
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord)]
+#[cfg_attr(
+ all(feature = "serde", feature = "alloc", feature = "multibase"),
+ derive(serde::Serialize, serde::Deserialize),
+ serde(into = "String", try_from = "String")
+)]
+#[cfg_attr(
+ all(feature = "schemars", feature = "serde", feature = "alloc", feature = "multibase"),
+ derive(schemars::JsonSchema),
+ schemars(
+ title = "Ed25519",
+ description = "An Ed25519 public key in multibase encoding.",
+ extend("examples" = [
+ "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7",
+ "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C",
+ "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
+ ]),
+ ),
+)]
+#[repr(transparent)]
+pub struct PublicKey(PublicKeyBytes);
+
+impl PublicKey {
+ pub const fn from_bytes(bytes: PublicKeyBytes) -> Self {
+ Self(bytes)
+ }
+
+ pub fn into_inner(self) -> PublicKeyBytes {
+ self.0
}
}
-pub trait Signer: Send + signature::Signer<Signature> {
- /// Return this signer's public/verification key.
- fn public_key(&self) -> &PublicKey;
+impl<'a> From<&'a PublicKeyBytes> for &'a PublicKey {
+ fn from(other: &'a PublicKeyBytes) -> Self {
+ let ptr = std::ptr::from_ref(other).cast::<PublicKey>();
+ // SAFETY: `PublicKey` is `#[repr(transparent)]` over the same array type,
+ // so the cast preserves layout and alignment, and every byte pattern is valid.
+ unsafe { &*ptr }
+ }
}
-impl<S> Signer for S
-where
- S: Send,
- S: signature::Signer<Signature>,
- S: signature::KeypairRef<VerifyingKey = PublicKey>,
-{
- fn public_key(&self) -> &PublicKey {
- self.as_ref()
+impl From<PublicKeyBytes> for PublicKey {
+ fn from(bytes: PublicKeyBytes) -> Self {
+ Self(bytes)
}
}
-/// Cryptographic signature.
-#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
-#[serde(into = "String", try_from = "String")]
-pub struct Signature(pub ed25519::Signature);
+#[cfg(feature = "alloc")]
+impl alloc::borrow::Borrow<PublicKeyBytes> for PublicKey {
+ fn borrow(&self) -> &PublicKeyBytes {
+ &self.0
+ }
+}
-impl AsRef<[u8]> for Signature {
- fn as_ref(&self) -> &[u8] {
- self.0.as_ref()
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::fmt::Display for PublicKey {
+ fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
+ write!(f, "{}", self.to_human())
}
}
-impl fmt::Display for Signature {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let base = multibase::Base::Base58Btc;
- write!(f, "{}", multibase::encode(base, self.deref()))
+#[cfg(feature = "ssh")]
+impl From<PublicKey> for ssh_key::public::Ed25519PublicKey {
+ fn from(key: PublicKey) -> Self {
+ ssh_key::public::Ed25519PublicKey(key.0)
}
}
-impl fmt::Debug for Signature {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Signature({self})")
+#[cfg(feature = "ssh")]
+impl From<ssh_key::public::Ed25519PublicKey> for PublicKey {
+ fn from(key: ssh_key::public::Ed25519PublicKey) -> Self {
+ Self(key.0)
}
}
-#[derive(Error, Debug)]
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
-pub enum SignatureError {
+pub enum PublicKeyError {
+ #[error("invalid length {0}")]
+ InvalidLength(usize),
#[error("invalid multibase string: {0}")]
- Multibase(#[from] multibase::Error),
- #[error("invalid signature: {0}")]
- Invalid(#[from] ed25519::Error),
+ Multibase(#[cfg_attr(feature = "std", source)] multibase::Error),
+ #[error("invalid multicodec prefix, expected {0:?}")]
+ Multicodec([u8; 2]),
+ #[error("invalid public key")]
+ Invalid(#[cfg_attr(feature = "std", source)] signature::Error),
}
-impl From<ed25519::Signature> for Signature {
- fn from(other: ed25519::Signature) -> Self {
- Self(other)
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl From<PublicKey> for String {
+ fn from(other: PublicKey) -> Self {
+ other.to_human()
}
}
-impl FromStr for Signature {
- type Err = SignatureError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (_, bytes) = multibase::decode(s)?;
- let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
+impl PublicKey {
+ /// Encode public key in human-readable format.
+ ///
+ /// `MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))`
+ ///
+ #[cfg(all(feature = "alloc", feature = "multibase"))]
+ pub fn to_human(&self) -> String {
+ let mut buf = [0; 2 + dalek::ed::PUBLIC_KEY_LENGTH];
+ buf[..2].copy_from_slice(&MULTICODEC_TYPE);
+ buf[2..].copy_from_slice(&self.0);
- Ok(Self(sig))
+ multibase::encode(multibase::Base::Base58Btc, buf)
}
-}
-impl Deref for Signature {
- type Target = ed25519::Signature;
+ #[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+ ))]
+ pub fn to_namespace(&self) -> git_ref_format_core::RefString {
+ use alloc::borrow::ToOwned as _;
+ use git_ref_format_core::name::{NAMESPACES, REFS};
+ REFS.to_owned().and(NAMESPACES).and(self.to_component())
+ }
- fn deref(&self) -> &Self::Target {
- &self.0
+ #[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+ ))]
+ pub fn to_component(&self) -> git_ref_format_core::Component<'_> {
+ git_ref_format_core::Component::from(self)
}
-}
-impl From<[u8; 64]> for Signature {
- fn from(bytes: [u8; 64]) -> Self {
- Self(ed25519::Signature::new(bytes))
+ #[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+ ))]
+ pub fn from_namespaced(
+ refstr: &git_ref_format_core::Namespaced,
+ ) -> Result<Self, PublicKeyError> {
+ use alloc::str::FromStr as _;
+
+ let name = refstr.namespace().into_inner();
+ Self::from_str(name.as_str())
}
}
-impl TryFrom<&[u8]> for Signature {
- type Error = ed25519::Error;
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::str::FromStr for PublicKey {
+ type Err = PublicKeyError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (_, bytes) = multibase::decode(s).map_err(PublicKeyError::Multibase)?;
- fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
- ed25519::Signature::from_slice(bytes).map(Self)
+ if bytes.len() < 2 {
+ return Err(PublicKeyError::InvalidLength(bytes.len()));
+ }
+
+ if bytes[..MULTICODEC_TYPE.len()] != MULTICODEC_TYPE {
+ return Err(PublicKeyError::Multicodec(MULTICODEC_TYPE));
+ }
+
+ Ok(PublicKey(
+ bytes[MULTICODEC_TYPE.len()..]
+ .try_into()
+ .map_err(|_| PublicKeyError::InvalidLength(bytes.len()))?,
+ ))
}
}
-impl From<Signature> for String {
- fn from(s: Signature) -> Self {
- s.to_string()
+#[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+))]
+impl From<&PublicKey> for git_ref_format_core::Component<'_> {
+ fn from(id: &PublicKey) -> Self {
+ use git_ref_format_core::{Component, RefString};
+ let refstr =
+ RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
+ Component::from_refstr(refstr).expect("encoded public keys are valid refname components")
}
}
-impl TryFrom<String> for Signature {
- type Error = SignatureError;
+#[cfg(all(feature = "sqlite", feature = "alloc", feature = "multibase"))]
+impl TryFrom<&sqlite::Value> for PublicKey {
+ type Error = sqlite::Error;
- fn try_from(s: String) -> Result<Self, Self::Error> {
- Self::from_str(&s)
+ fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
+ use alloc::str::FromStr as _;
+
+ match value {
+ sqlite::Value::String(s) => Self::from_str(s).map_err(|e| sqlite::Error {
+ code: None,
+ message: Some(e.to_string()),
+ }),
+ _ => Err(sqlite::Error {
+ code: None,
+ message: Some(String::from("sql: invalid type for public key")),
+ }),
+ }
}
}
-/// The public/verification key.
-#[derive(Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
-#[serde(into = "String", try_from = "String")]
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[cfg_attr(
- feature = "schemars",
- schemars(
- title = "Ed25519",
- description = "An Ed25519 public key in multibase encoding.",
- extend("examples" = [
- "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7",
- "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C",
- "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
- "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
- ]),
- ),
-)]
-pub struct PublicKey(amplify::Bytes32);
-
-impl PublicKey {
- /// Verify the signature for a given payload.
- pub fn verify(
- &self,
- payload: impl AsRef<[u8]>,
- signature: &ed25519::Signature,
- ) -> Result<(), ed25519::Error> {
- ed25519::PublicKey::new(self.0.to_byte_array()).verify(payload, signature)
+#[cfg(all(feature = "sqlite", feature = "alloc", feature = "multibase"))]
+impl sqlite::BindableWithIndex for &PublicKey {
+ fn bind<I: sqlite::ParameterIndex>(
+ self,
+ stmt: &mut sqlite::Statement<'_>,
+ i: I,
+ ) -> sqlite::Result<()> {
+ sqlite::Value::from(self).bind(stmt, i)
}
+}
- /// Returns a byte array representation of the public key.
- #[inline]
- pub fn to_byte_array(&self) -> [u8; 32] {
- self.0.to_byte_array()
+#[cfg(all(feature = "sqlite", feature = "alloc", feature = "multibase"))]
+impl From<&PublicKey> for sqlite::Value {
+ fn from(pk: &PublicKey) -> Self {
+ sqlite::Value::String(pk.to_human())
}
}
-impl signature::Verifier<Signature> for PublicKey {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> {
- self.verify(msg, signature)
- .map_err(signature::Error::from_source)
+#[cfg(feature = "cyphernet")]
+impl AsRef<[u8]> for PublicKey {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
}
}
-#[cfg(feature = "cyphernet")]
+#[cfg(all(feature = "cyphernet", feature = "alloc", feature = "multibase"))]
impl cyphernet::display::MultiDisplay<cyphernet::display::Encoding> for PublicKey {
type Display = String;
- fn display_fmt(&self, _: &cyphernet::display::Encoding) -> Self::Display {
- self.to_string()
- }
-}
-
-#[cfg(feature = "ssh")]
-impl From<PublicKey> for ssh_key::PublicKey {
- fn from(key: PublicKey) -> Self {
- ssh_key::PublicKey::from(ssh_key::public::Ed25519PublicKey(key.to_byte_array()))
+ fn display_fmt(&self, encoding: &cyphernet::display::Encoding) -> Self::Display {
+ match encoding {
+ cyphernet::display::Encoding::Base58
+ | cyphernet::display::Encoding::Multibase(multibase::Base::Base58Btc) => {
+ self.to_string()
+ }
+ _ => unimplemented!(),
+ }
}
}
-#[cfg(feature = "cyphernet")]
+#[cfg(all(feature = "cyphernet", feature = "alloc"))]
impl cyphernet::EcPk for PublicKey {
- const COMPRESSED_LEN: usize = 32;
+ const COMPRESSED_LEN: usize = dalek::ed::PUBLIC_KEY_LENGTH;
const CURVE_NAME: &'static str = "Edwards25519";
- type Compressed = amplify::Bytes32;
+ type Compressed = PublicKey;
fn base_point() -> Self {
unimplemented!()
}
fn to_pk_compressed(&self) -> Self::Compressed {
- amplify::Bytes32::from_byte_array(self.to_byte_array())
+ *self
}
fn from_pk_compressed(pk: Self::Compressed) -> Result<Self, cyphernet::EcPkInvalid> {
- Ok(PublicKey::from(pk.to_byte_array()))
+ Ok(pk)
}
- fn from_pk_compressed_slice(slice: &[u8]) -> Result<Self, cyphernet::EcPkInvalid> {
- ed25519::PublicKey::from_slice(slice)
- .map_err(|_| cyphernet::EcPkInvalid::default())
- .map(Self::from)
+ fn from_pk_compressed_slice(pk: &[u8]) -> Result<Self, cyphernet::EcPkInvalid> {
+ Ok(PublicKey(
+ PublicKeyBytes::try_from(pk).map_err(|_| cyphernet::EcPkInvalid::default())?,
+ ))
}
}
-/// The private/signing key.
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub struct SecretKey(ed25519::SecretKey);
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
+pub struct VerifyingKey(dalek::ed::VerifyingKey);
-impl SecretKey {
- /// Elliptic-curve Diffie-Hellman.
- pub fn ecdh(&self, pk: &PublicKey) -> Result<[u8; 32], ed25519::Error> {
- let scalar = self.seed().scalar();
- let ge = edwards25519::GeP3::from_bytes_vartime(&pk.to_byte_array())
- .ok_or(Error::InvalidPublicKey)?;
+impl VerifyingKey {
+ #[allow(clippy::wrong_self_convention)] // Name copied from dalek.
+ #[inline]
+ pub(crate) fn to_bytes(&self) -> PublicKeyBytes {
+ self.0.to_bytes()
+ }
- Ok(edwards25519::ge_scalarmult(&scalar, &ge).to_bytes())
+ #[cfg(feature = "x25519")]
+ pub fn to_x25519(&self) -> (u8, dalek::x::PublicKey) {
+ (
+ AsRef::<PublicKeyBytes>::as_ref(self)[31] >> 7,
+ dalek::x::PublicKey::from(self.0.to_montgomery().to_bytes()),
+ )
+ }
+
+ #[cfg(feature = "x25519")]
+ pub fn from_x25519(sign: u8, pk: &dalek::x::PublicKey) -> Option<Self> {
+ dalek::curve::montgomery::MontgomeryPoint(pk.to_bytes())
+ .to_edwards(sign)
+ .map(|point| Self(dalek::ed::VerifyingKey::from(point)))
}
}
-impl PartialOrd for SecretKey {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
+impl TryFrom<&PublicKey> for VerifyingKey {
+ type Error = signature::Error;
+
+ fn try_from(key: &PublicKey) -> Result<Self, Self::Error> {
+ dalek::ed::VerifyingKey::from_bytes(&key.0).map(Self)
}
}
-impl Ord for SecretKey {
- fn cmp(&self, other: &Self) -> Ordering {
- self.0.cmp(&other.0)
+impl<'a> VerifyingKey {
+ pub fn public_key(&'a self) -> &'a PublicKey {
+ self.0.as_bytes().into()
}
}
-impl zeroize::Zeroize for SecretKey {
- fn zeroize(&mut self) {
- self.0.zeroize();
+impl AsRef<PublicKeyBytes> for VerifyingKey {
+ fn as_ref(&self) -> &PublicKeyBytes {
+ self.0.as_bytes()
}
}
-impl TryFrom<&[u8]> for SecretKey {
- type Error = ed25519::Error;
+impl From<dalek::ed::VerifyingKey> for VerifyingKey {
+ fn from(other: dalek::ed::VerifyingKey) -> Self {
+ Self(other)
+ }
+}
- fn try_from(bytes: &[u8]) -> Result<Self, ed25519::Error> {
- ed25519::SecretKey::from_slice(bytes).map(Self)
+impl signature::Verifier<Signature> for VerifyingKey {
+ fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> {
+ self.0.verify(msg, signature)
}
}
-impl AsRef<[u8]> for SecretKey {
- fn as_ref(&self) -> &[u8] {
- &*self.0
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::fmt::Display for VerifyingKey {
+ fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
+ self.public_key().fmt(f)
}
}
-impl From<[u8; 64]> for SecretKey {
- fn from(bytes: [u8; 64]) -> Self {
- Self(ed25519::SecretKey::new(bytes))
+#[cfg(all(feature = "cyphernet", feature = "alloc", feature = "multibase"))]
+impl cyphernet::display::MultiDisplay<cyphernet::display::Encoding> for VerifyingKey {
+ type Display = String;
+
+ fn display_fmt(&self, encoding: &cyphernet::display::Encoding) -> Self::Display {
+ self.public_key().display_fmt(encoding)
}
}
-impl From<ed25519::SecretKey> for SecretKey {
- fn from(other: ed25519::SecretKey) -> Self {
- Self(other)
+#[cfg(feature = "cyphernet")]
+impl From<&cyphernet::ed25519::PublicKey> for PublicKey {
+ fn from(value: &cyphernet::ed25519::PublicKey) -> Self {
+ use core::ops::Deref as _;
+ Self(*value.deref().deref())
}
}
-impl From<SecretKey> for ed25519::SecretKey {
- fn from(other: SecretKey) -> Self {
- other.0
+#[cfg(feature = "cyphernet")]
+impl From<PublicKey> for cyphernet::ed25519::PublicKey {
+ fn from(value: PublicKey) -> Self {
+ use cyphernet::EcPk as _;
+ cyphernet::ed25519::PublicKey::from_pk_compressed(value.into_inner().into())
+ .expect("implementation is infallible")
}
}
-impl Deref for SecretKey {
- type Target = ed25519::SecretKey;
+#[cfg(all(feature = "cyphernet", feature = "alloc"))]
+impl cyphernet::EcPk for VerifyingKey {
+ const COMPRESSED_LEN: usize = dalek::ed::PUBLIC_KEY_LENGTH;
+ const CURVE_NAME: &'static str = "Edwards25519";
- fn deref(&self) -> &Self::Target {
- &self.0
+ type Compressed = PublicKey;
+
+ fn base_point() -> Self {
+ unimplemented!()
+ }
+
+ fn to_pk_compressed(&self) -> Self::Compressed {
+ *self.public_key()
+ }
+
+ fn from_pk_compressed(pk: Self::Compressed) -> Result<Self, cyphernet::EcPkInvalid> {
+ dalek::ed::VerifyingKey::from_bytes(&pk.0)
+ .map_err(|_| cyphernet::EcPkInvalid::default())
+ .map(Self)
+ }
+
+ fn from_pk_compressed_slice(slice: &[u8]) -> Result<Self, cyphernet::EcPkInvalid> {
+ Self::from_pk_compressed(PublicKey(
+ slice
+ .try_into()
+ .map_err(|_| cyphernet::EcPkInvalid::default())?,
+ ))
}
}
-#[derive(Error, Debug)]
+#[cfg(all(feature = "ssh", feature = "std"))]
+#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
-pub enum PublicKeyError {
- #[error("invalid length {0}")]
- InvalidLength(usize),
- #[error("invalid multibase string: {0}")]
- Multibase(#[from] multibase::Error),
- #[error("invalid multicodec prefix, expected {0:?}")]
- Multicodec([u8; 2]),
- #[error("invalid key: {0}")]
- InvalidKey(#[from] ed25519::Error),
-}
+pub enum LoadError {
+ #[error(transparent)]
+ Keystore(#[from] ssh::keystore::Error),
+ #[error("key not found in '{0}'")]
+ NotFound(std::path::PathBuf),
+ #[error("invalid passphrase")]
+ InvalidPassphrase,
+ #[error("secret key '{secret}' and public key '{public}' do not match")]
+ KeyMismatch {
+ secret: std::path::PathBuf,
+ public: std::path::PathBuf,
+ },
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SigningKey(dalek::ed::SigningKey);
+
+impl SigningKey {
+ fn public_key(&self) -> &PublicKey {
+ self.0.as_ref().as_bytes().into()
+ }
-impl fmt::Display for PublicKey {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.to_human())
+ pub fn from_seed(seed: Seed) -> Self {
+ Self(dalek::ed::SigningKey::from_bytes(seed.as_ref()))
}
-}
-impl From<PublicKey> for String {
- fn from(other: PublicKey) -> Self {
- other.to_human()
+ #[cfg(any(test, all(feature = "test", feature = "alloc")))]
+ pub fn mock(id: usize) -> Self {
+ let bytes = core::iter::repeat_n(
+ id,
+ dalek::ed::SECRET_KEY_LENGTH / core::mem::size_of::<usize>(),
+ )
+ .flat_map(|id| id.to_be_bytes())
+ .collect::<Vec<_>>();
+
+ Self(dalek::ed::SigningKey::from_bytes(
+ bytes.as_slice().try_into().unwrap(),
+ ))
+ }
+
+ pub fn to_keypair_bytes(&self) -> [u8; dalek::ed::KEYPAIR_LENGTH] {
+ self.0.to_keypair_bytes()
+ }
+
+ pub fn as_bytes(&self) -> &[u8; dalek::ed::SECRET_KEY_LENGTH] {
+ self.0.as_bytes()
+ }
+
+ /// Load this signer from a keystore, given a secret key passphrase.
+ #[cfg(all(feature = "ssh", feature = "std"))]
+ pub fn load(
+ keystore: &ssh::Keystore,
+ passphrase: Option<ssh::Passphrase>,
+ ) -> Result<Self, LoadError> {
+ let secret = keystore
+ .secret_key(passphrase)
+ .map_err(|e| {
+ if e.is_crypto_err() {
+ LoadError::InvalidPassphrase
+ } else {
+ e.into()
+ }
+ })?
+ .ok_or_else(|| LoadError::NotFound(keystore.secret_key_path().to_path_buf()))?;
+
+ let Some(public_path) = keystore.public_key_path() else {
+ // There is no public key in the key store, so there's nothing
+ // to validate. Derive it from the secret key.
+ return Ok(secret);
+ };
+
+ let public = keystore
+ .public_key()?
+ .ok_or_else(|| LoadError::NotFound(public_path.to_path_buf()))?;
+
+ if secret.public_key() != &public {
+ return Err(LoadError::KeyMismatch {
+ secret: keystore.secret_key_path().to_path_buf(),
+ public: public_path.to_path_buf(),
+ });
+ }
+
+ Ok(secret)
+ }
+
+ /// Elliptic-curve Diffie-Hellman.
+ #[cfg(feature = "diffie-hellman")]
+ pub fn diffie_hellman(&self, their_public: &VerifyingKey) -> Option<SharedSecret> {
+ let scalar = self.0.to_scalar();
+
+ dalek::curve::edwards::CompressedEdwardsY(their_public.to_bytes())
+ .decompress()
+ .map(|point| (scalar * point).compress().to_bytes())
+ }
+
+ #[cfg(feature = "x25519")]
+ pub fn to_x25519(&self) -> [u8; 32] {
+ dalek::curve::scalar::clamp_integer(self.0.to_scalar_bytes())
}
}
-impl fmt::Debug for PublicKey {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "PublicKey({self})")
+impl AsRef<PublicKey> for SigningKey {
+ fn as_ref(&self) -> &PublicKey {
+ self.public_key()
}
}
-impl From<ed25519::PublicKey> for PublicKey {
- fn from(other: ed25519::PublicKey) -> Self {
- Self(amplify::Bytes32::from_byte_array(*other.deref()))
+impl PartialOrd for SigningKey {
+ fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
+ Some(self.cmp(other))
}
}
-impl From<PublicKey> for ed25519::PublicKey {
- fn from(val: PublicKey) -> Self {
- ed25519::PublicKey::new(val.to_byte_array())
+impl Ord for SigningKey {
+ fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+ self.0.as_bytes().cmp(other.0.as_bytes())
}
}
-impl From<[u8; 32]> for PublicKey {
- fn from(other: [u8; 32]) -> Self {
- Self(amplify::Bytes32::from_byte_array(other))
+impl TryFrom<[u8; dalek::ed::KEYPAIR_LENGTH]> for SigningKey {
+ type Error = signature::Error;
+
+ fn try_from(bytes: [u8; dalek::ed::KEYPAIR_LENGTH]) -> Result<Self, Self::Error> {
+ dalek::ed::SigningKey::from_keypair_bytes(&bytes).map(Self)
}
}
-impl TryFrom<&[u8]> for PublicKey {
- type Error = ed25519::Error;
+impl From<dalek::ed::SigningKey> for SigningKey {
+ fn from(other: dalek::ed::SigningKey) -> Self {
+ Self(other)
+ }
+}
- fn try_from(other: &[u8]) -> Result<Self, Self::Error> {
- ed25519::PublicKey::from_slice(other).map(Self::from)
+impl From<SigningKey> for dalek::ed::SigningKey {
+ fn from(other: SigningKey) -> Self {
+ other.0
}
}
-impl PublicKey {
- /// Multicodec key type for Ed25519 keys.
- pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x1];
+impl signature::Signer<Signature> for SigningKey {
+ fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+ self.0.try_sign(msg)
+ }
+}
- /// Encode public key in human-readable format.
- ///
- /// `MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))`
- ///
- pub fn to_human(&self) -> String {
- let mut buf = [0; 2 + ed25519::PublicKey::BYTES];
- buf[..2].copy_from_slice(&Self::MULTICODEC_TYPE);
- buf[2..].copy_from_slice(self.to_byte_array().as_slice());
+impl signature::Keypair for SigningKey {
+ type VerifyingKey = VerifyingKey;
- multibase::encode(multibase::Base::Base58Btc, buf)
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ VerifyingKey(self.0.verifying_key())
}
+}
- #[cfg(feature = "git-ref-format-core")]
- pub fn to_namespace(&self) -> git_ref_format_core::RefString {
- use git_ref_format_core::name::{Component, NAMESPACES, REFS};
- REFS.to_owned().and(NAMESPACES).and(Component::from(self))
+#[cfg(all(feature = "cyphernet", feature = "alloc"))]
+impl cyphernet::EcSk for SigningKey {
+ type Pk = VerifyingKey;
+
+ fn generate_keypair() -> (Self, Self::Pk)
+ where
+ Self: Sized,
+ {
+ let signing_key = dalek::ed::SigningKey::from_bytes(&[154; 32]);
+ let verifying_key = signature::Keypair::verifying_key(&signing_key);
+ (SigningKey(signing_key), VerifyingKey(verifying_key))
}
- #[cfg(feature = "git-ref-format-core")]
- pub fn to_component(&self) -> git_ref_format_core::Component<'_> {
- git_ref_format_core::Component::from(self)
+ fn to_pk(&self) -> Result<Self::Pk, cyphernet::EcSkInvalid> {
+ use signature::Keypair as _;
+
+ Ok(self.verifying_key())
}
+}
- #[cfg(feature = "git-ref-format-core")]
- pub fn from_namespaced(
- refstr: &git_ref_format_core::Namespaced,
- ) -> Result<Self, PublicKeyError> {
- let name = refstr.namespace().into_inner();
+#[cfg(all(feature = "cyphernet", feature = "diffie-hellman"))]
+impl cyphernet::Ecdh for SigningKey {
+ type SharedSecret = SharedSecret;
- Self::from_str(name.deref().as_str())
+ fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
+ self.diffie_hellman(pk)
+ .ok_or(cyphernet::EcdhError::InvalidPk(
+ cyphernet::EcPkInvalid::default(),
+ ))
}
}
-impl FromStr for PublicKey {
+#[cfg(any(test, feature = "test"))]
+impl qcheck::Arbitrary for SigningKey {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ SigningKey::mock(usize::arbitrary(g))
+ }
+}
+
+#[cfg(all(feature = "alloc", feature = "multibase", feature = "cyphernet"))]
+impl alloc::str::FromStr for VerifyingKey {
type Err = PublicKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (_, bytes) = multibase::decode(s)?;
-
- if let Some(bytes) = bytes.strip_prefix(&Self::MULTICODEC_TYPE) {
- let key = ed25519::PublicKey::from_slice(bytes)?;
-
- Ok(key.into())
- } else {
- Err(PublicKeyError::Multicodec(Self::MULTICODEC_TYPE))
- }
+ let pk = PublicKey::from_str(s)?;
+ dalek::ed::VerifyingKey::from_bytes(&pk.0)
+ .map(Self)
+ .map_err(PublicKeyError::Invalid)
}
}
+#[cfg(all(feature = "alloc", feature = "multibase"))]
impl TryFrom<String> for PublicKey {
type Error = PublicKeyError;
fn try_from(value: String) -> Result<Self, Self::Error> {
+ use alloc::str::FromStr as _;
+
Self::from_str(&value)
}
}
-#[cfg(feature = "git-ref-format-core")]
-impl From<&PublicKey> for git_ref_format_core::Component<'_> {
- fn from(id: &PublicKey) -> Self {
- use git_ref_format_core::{Component, RefString};
- let refstr =
- RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
- Component::from_refstr(refstr).expect("encoded public keys are valid refname components")
+#[cfg(all(feature = "alloc", feature = "multibase", feature = "cyphernet"))]
+impl TryFrom<String> for VerifyingKey {
+ type Error = PublicKeyError;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ use alloc::str::FromStr as _;
+
+ Self::from_str(&value)
}
}
-#[cfg(feature = "sqlite")]
-impl From<&PublicKey> for sqlite::Value {
- fn from(pk: &PublicKey) -> Self {
- sqlite::Value::String(pk.to_human())
+#[cfg(any(test, feature = "test"))]
+impl qcheck::Arbitrary for PublicKey {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ *SigningKey::arbitrary(g).public_key()
}
}
-#[cfg(feature = "sqlite")]
-impl TryFrom<&sqlite::Value> for PublicKey {
- type Error = sqlite::Error;
+/// An extended signature carries the key that may be used to verify the
+/// signature along with the signature itself.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ExtendedSignature<PublicKey = crate::PublicKey, Signature = crate::Signature> {
+ key: PublicKey,
+ sig: Signature,
+}
- fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
- match value {
- sqlite::Value::String(s) => Self::from_str(s).map_err(|e| sqlite::Error {
- code: None,
- message: Some(e.to_string()),
- }),
- _ => Err(sqlite::Error {
- code: None,
- message: Some("sql: invalid type for public key".to_owned()),
- }),
- }
+impl ExtendedSignature {
+ pub fn try_sign(signer: &impl Signer, payload: &[u8]) -> Result<Self, signature::Error> {
+ Ok(Self {
+ key: *signer.public_key(),
+ sig: signer.try_sign(payload)?,
+ })
}
}
-#[cfg(feature = "sqlite")]
-impl sqlite::BindableWithIndex for &PublicKey {
- fn bind<I: sqlite::ParameterIndex>(
- self,
- stmt: &mut sqlite::Statement<'_>,
- i: I,
- ) -> sqlite::Result<()> {
- sqlite::Value::from(self).bind(stmt, i)
+impl<VerifyingKey, Signature> ExtendedSignature<VerifyingKey, Signature>
+where
+ VerifyingKey: signature::Verifier<Signature>,
+{
+ /// Verify the signature for a given payload.
+ pub fn verify(&self, msg: &[u8]) -> Result<(), signature::Error> {
+ self.key.verify(msg, &self.sig)
}
}
-#[cfg(feature = "sqlite")]
-impl From<&Signature> for sqlite::Value {
- fn from(sig: &Signature) -> Self {
- sqlite::Value::Binary(sig.to_vec())
+impl<VerifyingKey, Signature> ExtendedSignature<VerifyingKey, Signature> {
+ /// Create a new extended signature.
+ pub fn new(key: VerifyingKey, sig: Signature) -> Self {
+ Self { key, sig }
}
-}
-#[cfg(feature = "sqlite")]
-impl TryFrom<&sqlite::Value> for Signature {
- type Error = sqlite::Error;
+ pub fn key(&self) -> &VerifyingKey {
+ &self.key
+ }
- fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
- match value {
- sqlite::Value::Binary(s) => ed25519::Signature::from_slice(s)
- .map_err(|e| sqlite::Error {
- code: None,
- message: Some(e.to_string()),
- })
- .map(Self),
- _ => Err(sqlite::Error {
- code: None,
- message: Some("sql: invalid column type for signature".to_owned()),
- }),
- }
+ pub fn sig(&self) -> &Signature {
+ &self.sig
}
-}
-#[cfg(feature = "sqlite")]
-impl sqlite::BindableWithIndex for &Signature {
- fn bind<I: sqlite::ParameterIndex>(
- self,
- stmt: &mut sqlite::Statement<'_>,
- i: I,
- ) -> sqlite::Result<()> {
- sqlite::Value::from(self).bind(stmt, i)
+ pub fn into_pair(self) -> (VerifyingKey, Signature) {
+ (self.key, self.sig)
}
}
#[cfg(test)]
mod tests {
- use super::KeyPair;
- use crate::{PublicKey, SecretKey};
+ use super::*;
+
use qcheck_macros::quickcheck;
- use std::str::FromStr;
- #[test]
- fn test_e25519_dh() {
- let kp_a = KeyPair::generate();
- let kp_b = KeyPair::generate();
+ use crate::{SigningKey, VerifyingKey};
+
+ /// See <https://w3c-ccg.github.io/did-key-spec/#example-a-simple-ed25519-did-key-value>.
+ const DID_KEY_SAMPLE: &str = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
- let output_a = SecretKey::from(kp_b.sk).ecdh(&kp_a.pk.into()).unwrap();
- let output_b = SecretKey::from(kp_a.sk).ecdh(&kp_b.pk.into()).unwrap();
+ #[cfg(feature = "diffie-hellman")]
+ #[quickcheck]
+ fn diffie_hellman(sk_a: SigningKey, sk_b: SigningKey) {
+ use signature::Keypair as _;
+
+ let output_a = sk_b.diffie_hellman(&sk_a.verifying_key()).unwrap();
+ let output_b = sk_a.diffie_hellman(&sk_b.verifying_key()).unwrap();
assert_eq!(output_a, output_b);
}
+ #[cfg(feature = "alloc")]
#[quickcheck]
fn prop_encode_decode(input: PublicKey) {
+ use alloc::str::FromStr as _;
+
let encoded = input.to_string();
let decoded = PublicKey::from_str(&encoded).unwrap();
assert_eq!(input, decoded);
}
+ #[cfg(feature = "alloc")]
#[test]
- fn test_encode_decode() {
- let input = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
- let key = PublicKey::from_str(input).unwrap();
+ fn did_key_sample() {
+ use alloc::str::FromStr as _;
- assert_eq!(key.to_string(), input);
+ let key = PublicKey::from_str(DID_KEY_SAMPLE).unwrap();
+
+ assert_eq!(key.to_string(), DID_KEY_SAMPLE);
}
+ #[cfg(feature = "std")]
#[quickcheck]
fn prop_key_equality(a: PublicKey, b: PublicKey) {
- use std::collections::HashSet;
-
- assert_ne!(a, b);
+ if a == b {
+ return;
+ }
- let mut hm = HashSet::new();
+ let mut hm = std::collections::HashSet::new();
assert!(hm.insert(a));
assert!(hm.insert(b));
@@ -545,8 +821,9 @@ mod tests {
assert!(!hm.insert(b));
}
+ #[cfg(feature = "diffie-hellman")]
#[test]
- fn e25519_dh_fixture() {
+ fn diffie_hellman_fixture() {
let sk_a: [u8; 32] = [
92, 136, 18, 88, 112, 205, 201, 68, 109, 197, 130, 211, 179, 138, 197, 113, 120, 55,
104, 139, 208, 184, 178, 157, 120, 11, 60, 13, 91, 30, 213, 38,
@@ -556,13 +833,18 @@ mod tests {
21, 202, 228, 123, 193, 140, 252, 63, 72, 5, 137, 36, 245,
];
- let kp_a = KeyPair::from_seed(ec25519::Seed::from(sk_a));
- let kp_b = KeyPair::from_seed(ec25519::Seed::from(sk_b));
+ let kp_a = dalek::ed::SigningKey::from_bytes(&sk_a);
+ let kp_b = dalek::ed::SigningKey::from_bytes(&sk_b);
- let output_a = SecretKey::from(kp_b.sk).ecdh(&kp_a.pk.into()).unwrap();
- let output_b = SecretKey::from(kp_a.sk).ecdh(&kp_b.pk.into()).unwrap();
+ let output_a = SigningKey::from(kp_b.clone())
+ .diffie_hellman(&kp_a.verifying_key().into())
+ .unwrap();
+ let output_b = SigningKey::from(kp_a)
+ .diffie_hellman(&kp_b.verifying_key().into())
+ .unwrap();
assert_eq!(output_a, output_b);
+
assert_eq!(
output_a,
[
@@ -571,4 +853,72 @@ mod tests {
]
)
}
+
+ // See <https://docs.rs/ed25519_to_curve25519/0.2.4/src/ed25519_to_curve25519/lib.rs.html#15-26>.
+ #[cfg(all(feature = "alloc", feature = "x25519"))]
+ #[test]
+ fn x25519_pk() {
+ let ed25519_pk = [
+ 59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13, 115, 101, 50, 21,
+ 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41,
+ ];
+
+ let curve25519_pk = [
+ 91, 245, 92, 115, 184, 46, 190, 34, 190, 128, 243, 67, 6, 103, 175, 87, 15, 174, 37,
+ 86, 166, 65, 94, 107, 48, 212, 6, 83, 0, 170, 148, 125,
+ ];
+
+ let ed25519_pk = VerifyingKey::try_from(&PublicKey::from(ed25519_pk)).unwrap();
+ let converted = ed25519_pk.to_x25519();
+
+ assert_eq!(*converted.1.as_bytes(), curve25519_pk);
+
+ assert_eq!(
+ VerifyingKey::from_x25519(converted.0, &converted.1).unwrap(),
+ ed25519_pk
+ );
+ }
+
+ // See <https://docs.rs/ed25519_to_curve25519/0.2.4/src/ed25519_to_curve25519/lib.rs.html#49-62>.
+ #[cfg(all(feature = "alloc", feature = "x25519"))]
+ #[test]
+ fn x25519_sk() {
+ let ed25519_sk: [u8; 32] = [
+ 202, 104, 239, 81, 53, 110, 80, 252, 198, 23, 155, 162, 215, 98, 223, 173, 227, 188,
+ 110, 54, 127, 45, 185, 206, 174, 29, 44, 147, 76, 66, 196, 195,
+ ];
+
+ let curve25519_sk: [u8; 32] = [
+ 200, 255, 64, 61, 17, 52, 112, 33, 205, 71, 186, 13, 131, 12, 241, 136, 223, 5, 152,
+ 40, 95, 187, 83, 168, 142, 10, 234, 215, 70, 210, 148, 104,
+ ];
+
+ let ed25519_sk = SigningKey::from(dalek::ed::SigningKey::from_bytes(&ed25519_sk));
+
+ assert_eq!(ed25519_sk.to_x25519(), curve25519_sk);
+ }
+
+ /// See <https://w3c-ccg.github.io/did-key-spec/#ed25519-with-x25519>.
+ #[cfg(all(feature = "alloc", feature = "x25519", feature = "multibase"))]
+ #[test]
+ fn ed25519_did() {
+ let ed: PublicKey = DID_KEY_SAMPLE.parse().unwrap();
+
+ let expected = {
+ // X25519 public key, multibase encoded according to `did:key` spec.
+ let key = "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p";
+
+ let mut bytes = multibase::decode(key).unwrap().1;
+
+ // X25519 public key, as raw bytes.
+ let key = bytes.split_off(2);
+
+ // The first two bytes indicate that this is an X25519 public key.
+ assert_eq!(bytes, [0xEC, 0x01]);
+
+ dalek::x::PublicKey::from(TryInto::<[u8; 32]>::try_into(key).unwrap())
+ };
+
+ assert_eq!(expected, VerifyingKey::try_from(&ed).unwrap().to_x25519().1);
+ }
}
diff --git a/crates/radicle-crypto/src/seed.rs b/crates/radicle-crypto/src/seed.rs
new file mode 100644
index 000000000..a5451ac66
--- /dev/null
+++ b/crates/radicle-crypto/src/seed.rs
@@ -0,0 +1,20 @@
+use zeroize::Zeroizing;
+
+pub struct Seed(Zeroizing<[u8; 32]>);
+
+impl Seed {
+ pub fn new(seed: [u8; 32]) -> Self {
+ Seed(Zeroizing::from(seed))
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn mock(byte: u8) -> Self {
+ Self::new([byte; 32])
+ }
+}
+
+impl AsRef<[u8; 32]> for Seed {
+ fn as_ref(&self) -> &[u8; 32] {
+ &self.0
+ }
+}
diff --git a/crates/radicle-crypto/src/ssh.rs b/crates/radicle-crypto/src/ssh.rs
index d6b436aa5..8d0ff8160 100644
--- a/crates/radicle-crypto/src/ssh.rs
+++ b/crates/radicle-crypto/src/ssh.rs
@@ -1,103 +1,85 @@
-pub mod agent;
-pub mod keystore;
+extern crate alloc;
+
+use alloc::string::{String, ToString as _};
use thiserror::Error;
-use crate as crypto;
+#[cfg(feature = "std")]
+pub mod agent;
+
+#[cfg(feature = "std")]
+pub mod keystore;
+#[cfg(feature = "std")]
pub use keystore::{Keystore, Passphrase};
#[derive(Debug, Error)]
#[non_exhaustive]
-pub enum ExtendedSignatureError {
+pub enum ExtendedSignaturePemError {
#[error(transparent)]
Ssh(#[from] ssh_key::Error),
#[error(transparent)]
- Crypto(#[from] crypto::Error),
- #[error("unsupported signature algorithm")]
- UnsupportedAlgorithm,
-}
-
-/// Signature with public key, used for SSH signing.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ExtendedSignature {
- pub key: crypto::PublicKey,
- pub sig: crypto::Signature,
-}
-
-impl From<ExtendedSignature> for crypto::Signature {
- fn from(ExtendedSignature { sig, .. }: ExtendedSignature) -> Self {
- sig
- }
+ Signature(#[from] crate::signature::Error),
+ #[error("unsupported signature algorithm: {algorithm}")]
+ UnsupportedAlgorithm { algorithm: ssh_key::Algorithm },
}
-impl ExtendedSignature {
- /// Create a new extended signature.
- pub fn new(public_key: crypto::PublicKey, signature: crypto::Signature) -> Self {
- Self {
- key: public_key,
- sig: signature,
- }
- }
-
+impl crate::ExtendedSignature {
/// Convert to OpenSSH standard PEM format.
- pub fn to_pem(&self) -> Result<String, ExtendedSignatureError> {
+ pub fn to_pem(&self) -> Result<String, ExtendedSignaturePemError> {
ssh_key::SshSig::new(
- ssh_key::public::KeyData::from(ssh_key::public::Ed25519PublicKey(
- self.key.to_byte_array(),
- )),
+ ssh_key::public::KeyData::from(ssh_key::public::Ed25519PublicKey::from(self.key)),
String::from("radicle"),
ssh_key::HashAlg::Sha256,
- ssh_key::Signature::new(ssh_key::Algorithm::Ed25519, **self.sig)?,
+ ssh_key::Signature::new(ssh_key::Algorithm::Ed25519, self.sig.to_vec())?,
)?
.to_pem(ssh_key::LineEnding::default())
- .map_err(ExtendedSignatureError::from)
+ .map_err(ExtendedSignaturePemError::from)
}
/// Create from OpenSSH PEM format.
- pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self, ExtendedSignatureError> {
+ pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self, ExtendedSignaturePemError> {
let sig = ssh_key::SshSig::from_pem(pem)?;
+ let key = match sig.public_key() {
+ ssh_key::public::KeyData::Ed25519(key) => key.0,
+ key_data => {
+ return Err(ExtendedSignaturePemError::UnsupportedAlgorithm {
+ algorithm: key_data.algorithm(),
+ });
+ }
+ };
+
Ok(Self {
- key: crypto::PublicKey::from(
- sig.public_key()
- .ed25519()
- .ok_or(ExtendedSignatureError::UnsupportedAlgorithm)?
- .0,
- ),
- sig: crypto::Signature::try_from(sig.signature().as_bytes())?,
+ key: crate::PublicKey(key),
+ sig: crate::Signature::try_from(sig.signature().as_bytes())?,
})
}
-
- /// Verify the signature for a given payload.
- pub fn verify(&self, payload: &[u8]) -> bool {
- self.key.verify(payload, &self.sig).is_ok()
- }
}
pub mod fmt {
+ use super::*;
use crate::PublicKey;
/// Get the SSH long key from a public key.
/// This is the output of `ssh-add -L`.
pub fn key(key: &PublicKey) -> String {
- ssh_key::PublicKey::from(*key).to_string()
+ ssh_key::PublicKey::from(ssh_key::public::Ed25519PublicKey(key.0)).to_string()
}
/// Get the SSH key fingerprint from a public key.
/// This is the output of `ssh-add -l`.
pub fn fingerprint(key: &PublicKey) -> String {
- ssh_key::PublicKey::from(*key)
+ ssh_key::PublicKey::from(ssh_key::public::Ed25519PublicKey(key.0))
.fingerprint(Default::default())
.to_string()
}
#[cfg(test)]
mod test {
- use std::str::FromStr;
-
use super::*;
- use crate::PublicKey;
+
+ use alloc::str::FromStr;
#[test]
fn test_key() {
@@ -114,6 +96,7 @@ pub mod fmt {
fn test_fingerprint() {
let pk =
PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+
assert_eq!(
fingerprint(&pk),
"SHA256:gE/Ty4fuXzww49lcnNe9/GI0L7xSEQdFp/v9tOjFwB4"
diff --git a/crates/radicle-crypto/src/ssh/agent.rs b/crates/radicle-crypto/src/ssh/agent.rs
index fe48bb8df..8d07ff1a2 100644
--- a/crates/radicle-crypto/src/ssh/agent.rs
+++ b/crates/radicle-crypto/src/ssh/agent.rs
@@ -1,7 +1,12 @@
+extern crate std;
+
+use std::borrow::ToOwned as _;
use std::cell::RefCell;
use std::env::VarError;
use std::path::Path;
use std::path::PathBuf;
+use std::string::{String, ToString as _};
+use std::vec::Vec;
use proto::{PrivateCredential, PublicCredential};
use ssh_agent_lib::blocking::Client;
@@ -10,9 +15,7 @@ use ssh_agent_lib::proto;
use ssh_key::public::{Ed25519PublicKey, KeyData};
use thiserror::Error;
-use crate::{PublicKey, SecretKey, Signature, Signer};
-
-use super::ExtendedSignature;
+use crate::{PublicKey, Signature, SigningKey, VerifyingKey};
#[cfg(unix)]
use std::os::unix::net::UnixStream as Stream;
@@ -50,6 +53,14 @@ impl ConnectError {
}
}
+#[derive(Debug, Error)]
+pub enum IntoSignerError {
+ #[error(transparent)]
+ Agent(#[from] AgentError),
+ #[error("Identity {identity} not found in agent.")]
+ IdentityNotFound { identity: PublicKey },
+}
+
pub struct Agent {
path: PathBuf,
client: Client<Stream>,
@@ -82,17 +93,22 @@ impl Agent {
}
/// Register a key with the agent.
- pub fn register(&mut self, key: &SecretKey) -> Result<(), AgentError> {
+ pub fn register(&mut self, key: &SigningKey) -> Result<(), AgentError> {
use ssh_key::private::{Ed25519Keypair, KeypairData};
self.client.add_identity(proto::AddIdentity {
credential: PrivateCredential::Key {
- privkey: KeypairData::Ed25519(Ed25519Keypair::from_bytes(key).unwrap()),
+ privkey: KeypairData::Ed25519(
+ Ed25519Keypair::from_bytes(
+ &ed25519_dalek::SigningKey::from(key.clone()).to_keypair_bytes(),
+ )
+ .unwrap(),
+ ),
comment: "".into(),
},
})
}
- pub fn unregister(&mut self, key: &PublicKey) -> Result<(), AgentError> {
+ pub fn unregister(&mut self, key: &VerifyingKey) -> Result<(), AgentError> {
self.client.remove_identity(proto::RemoveIdentity {
credential: PublicCredential::Key(Self::key_data(key)),
})
@@ -102,7 +118,7 @@ impl Agent {
self.client.remove_all_identities()
}
- pub fn sign(&mut self, key: &PublicKey, data: &[u8]) -> Result<[u8; 64], AgentError> {
+ pub fn sign(&mut self, key: &VerifyingKey, data: &[u8]) -> Result<[u8; 64], AgentError> {
let sig = self.client.sign(proto::SignRequest {
credential: PublicCredential::Key(Self::key_data(key)),
data: data.to_vec(),
@@ -112,9 +128,16 @@ impl Agent {
Ok(sig.as_bytes().to_owned().try_into().unwrap())
}
- /// Get a signer from this agent, given the public key.
- pub fn signer(self, key: PublicKey) -> AgentSigner {
- AgentSigner::new(self, key)
+ /// Transform this [`Agent`] into an [`AgentSigner`] scoped to the given
+ /// `identity`.
+ pub fn into_signer(mut self, identity: VerifyingKey) -> Result<AgentSigner, IntoSignerError> {
+ if self.request_identities()?.contains(identity.public_key()) {
+ Ok(AgentSigner::new(self, identity))
+ } else {
+ Err(IntoSignerError::IdentityNotFound {
+ identity: *identity.public_key(),
+ })
+ }
}
pub fn path(&self) -> &Path {
@@ -136,79 +159,68 @@ impl Agent {
.collect())
}
- fn key_data(key: &PublicKey) -> KeyData {
- KeyData::Ed25519(Ed25519PublicKey(key.to_byte_array()))
+ fn key_data(key: &VerifyingKey) -> KeyData {
+ KeyData::Ed25519(Ed25519PublicKey(key.to_bytes()))
}
}
-/// A [`Signer`] that uses `ssh-agent`.
+/// A [`crate::signature::Signer`] that uses `ssh-agent`.
pub struct AgentSigner {
agent: RefCell<Agent>,
- public: PublicKey,
+ public: VerifyingKey,
}
-impl signature::Signer<Signature> for AgentSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+impl crate::signature::Signer<Signature> for AgentSigner {
+ fn try_sign(&self, msg: &[u8]) -> Result<Signature, crate::signature::Error> {
let sig = self
.agent
.borrow_mut()
.sign(&self.public, msg)
- .map_err(signature::Error::from_source)?;
+ .map_err(crate::signature::Error::from_source)?;
Ok(Signature::from(sig))
}
}
-impl signature::Signer<ExtendedSignature> for AgentSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
- use signature::Keypair as _;
- Ok(ExtendedSignature {
- key: self.verifying_key(),
- sig: self.try_sign(msg)?,
- })
+impl AsRef<VerifyingKey> for AgentSigner {
+ fn as_ref(&self) -> &VerifyingKey {
+ &self.public
}
}
-impl AsRef<PublicKey> for AgentSigner {
- fn as_ref(&self) -> &PublicKey {
- &self.public
+impl AsRef<crate::PublicKey> for AgentSigner {
+ fn as_ref(&self) -> &crate::PublicKey {
+ self.public.public_key()
}
}
-impl signature::KeypairRef for AgentSigner {
- type VerifyingKey = PublicKey;
+impl crate::signature::KeypairRef for AgentSigner {
+ type VerifyingKey = VerifyingKey;
}
impl AgentSigner {
- pub fn new(agent: Agent, public: PublicKey) -> Self {
+ pub fn new(agent: Agent, public: VerifyingKey) -> Self {
let agent = RefCell::new(agent);
Self { agent, public }
}
-
- pub fn is_ready(&self) -> Result<bool, AgentError> {
- let ids = self.agent.borrow_mut().request_identities()?;
-
- Ok(ids.contains(&self.public))
- }
-
- /// Box this signer into a [`Signer`].
- pub fn boxed(self) -> Box<dyn Signer> {
- Box::new(self)
- }
}
#[cfg(test)]
mod test {
- use crate::PublicKey;
+ use super::*;
+
use ssh_agent_lib::blocking::Client;
use ssh_agent_lib::proto::{PublicCredential, SignRequest};
use ssh_agent_lib::ssh_key::public::{Ed25519PublicKey, KeyData};
+ use crate::VerifyingKey;
+
#[test]
fn test_agent_encoding_remove() {
use std::str::FromStr;
- let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+ let pk =
+ VerifyingKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
let expected = [
0, 0, 0, 56, // Message length
18, // Message type (remove identity)
@@ -220,14 +232,14 @@ mod test {
37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key
];
- let mut client = Client::new(std::io::Cursor::new(vec![]));
+ let mut client = Client::new(std::io::Cursor::new(Vec::new()));
// We expect this to fail with an unexpected EOF, since the client will
// attempt to read a response from the stream, but the stream is empty,
// since we are not actually connected to SSH agent.
assert!(
matches!(client.remove_identity(ssh_agent_lib::proto::RemoveIdentity {
- credential: PublicCredential::Key(KeyData::Ed25519(Ed25519PublicKey(pk.to_byte_array()))),
+ credential: PublicCredential::Key(KeyData::Ed25519(Ed25519PublicKey(pk.to_bytes()))),
}),
Err(
super::AgentError::Proto(ssh_agent_lib::proto::ProtoError::IO(err)),
@@ -242,7 +254,8 @@ mod test {
fn test_agent_encoding_sign() {
use std::str::FromStr;
- let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+ let pk =
+ VerifyingKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
let expected = [
0, 0, 0, 73, // Message length
13, // Message type (sign request)
@@ -257,13 +270,13 @@ mod test {
0, 0, 0, 0, // Signature flags
];
- let mut client = Client::new(std::io::Cursor::new(vec![]));
- let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
+ let mut client = Client::new(std::io::Cursor::new(Vec::new()));
+ let data: Vec<u8> = [1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().collect();
client
.sign(SignRequest {
credential: PublicCredential::Key(KeyData::Ed25519(Ed25519PublicKey(
- pk.to_byte_array(),
+ pk.to_bytes(),
))),
data,
flags: 0,
diff --git a/crates/radicle-crypto/src/ssh/keystore.rs b/crates/radicle-crypto/src/ssh/keystore.rs
index c899d5f44..e43a80232 100644
--- a/crates/radicle-crypto/src/ssh/keystore.rs
+++ b/crates/radicle-crypto/src/ssh/keystore.rs
@@ -1,15 +1,13 @@
-use std::ops::Deref;
+extern crate std;
+
use std::path::{Path, PathBuf};
+use std::string::String;
use std::{fs, io};
-#[cfg(feature = "cyphernet")]
-use cyphernet::{EcSk, EcSkInvalid, Ecdh};
use thiserror::Error;
use zeroize::Zeroizing;
-use crate::{KeyPair, PublicKey, SecretKey, Signature, Signer};
-
-use super::ExtendedSignature;
+use crate::{PublicKey, Seed, SigningKey};
/// A secret key passphrase.
pub type Passphrase = Zeroizing<String>;
@@ -27,6 +25,10 @@ pub enum Error {
AlreadyInitialized { exists: PathBuf },
#[error("keystore is encrypted; a passphrase is required")]
PassphraseMissing,
+ #[error(transparent)]
+ Signature(#[from] crate::signature::Error),
+ #[error("invalid key")]
+ Invalid,
}
impl Error {
@@ -91,19 +93,22 @@ impl Keystore {
&self,
comment: &str,
passphrase: Option<Passphrase>,
- seed: ec25519::Seed,
+ seed: Seed,
) -> Result<PublicKey, Error> {
- self.store(KeyPair::from_seed(seed), comment, passphrase)
+ let signing_key = SigningKey::from_seed(seed);
+ self.store(&signing_key, comment, passphrase)?;
+ Ok(*signing_key.public_key())
}
/// Store a keypair on disk. Returns an error if any of the two key files already exist.
pub fn store(
&self,
- keypair: KeyPair,
+ keypair: &SigningKey,
comment: &str,
passphrase: Option<Passphrase>,
- ) -> Result<PublicKey, Error> {
- let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&keypair)?;
+ ) -> Result<(), Error> {
+ let keypair_bytes = keypair.to_keypair_bytes();
+ let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&keypair_bytes)?;
let ssh_pair = ssh_key::private::KeypairData::Ed25519(ssh_pair);
let secret = ssh_key::PrivateKey::new(ssh_pair, comment)?;
let secret = if let Some(p) = passphrase {
@@ -160,11 +165,14 @@ impl Keystore {
public.write_openssh_file(path_public)?;
}
- Ok(keypair.pk.into())
+ Ok(())
}
/// Load the public key from the store. Returns `None` if it wasn't found.
pub fn public_key(&self) -> Result<Option<PublicKey>, Error> {
+ use KeyData::*;
+ use ssh_key::{PublicKey as SshPublicKey, public::KeyData};
+
let Some(path_public) = &self.path_public else {
return Ok(None);
};
@@ -173,24 +181,24 @@ impl Keystore {
return Ok(None);
}
- let public = ssh_key::PublicKey::read_openssh_file(path_public)?;
- PublicKey::try_from(public)
- .map(Some)
- .map_err(|_| Error::InvalidKeyType)
+ match KeyData::from(SshPublicKey::read_openssh_file(path_public)?) {
+ Ed25519(key) => Ok(Some(PublicKey::from(key))),
+ _ => Err(Error::InvalidKeyType),
+ }
}
/// Load the secret key from the store, decrypting it with the given passphrase.
/// Returns `None` if it wasn't found.
- pub fn secret_key(
- &self,
- passphrase: Option<Passphrase>,
- ) -> Result<Option<Zeroizing<SecretKey>>, Error> {
+ pub fn secret_key(&self, passphrase: Option<Passphrase>) -> Result<Option<SigningKey>, Error> {
+ use KeypairData::*;
+ use ssh_key::{PrivateKey, private::KeypairData};
+
let path = &self.path_secret;
if !path.exists() {
return Ok(None);
}
- let secret = ssh_key::PrivateKey::read_openssh_file(path)?;
+ let secret = PrivateKey::read_openssh_file(path)?;
let secret = if let Some(p) = passphrase {
secret.decrypt(p)?
@@ -200,9 +208,7 @@ impl Keystore {
secret
};
match secret.key_data() {
- ssh_key::private::KeypairData::Ed25519(pair) => {
- Ok(Some(SecretKey::from(pair.to_bytes()).into()))
- }
+ Ed25519(pair) => Ok(Some(SigningKey::try_from(pair.to_bytes())?)),
_ => Err(Error::InvalidKeyType),
}
}
@@ -227,174 +233,19 @@ impl Keystore {
}
}
-#[derive(Debug, Error)]
-#[non_exhaustive]
-pub enum MemorySignerError {
- #[error(transparent)]
- Keystore(#[from] Error),
- #[error("key not found in '{0}'")]
- NotFound(PathBuf),
- #[error("invalid passphrase")]
- InvalidPassphrase,
- #[error("secret key '{secret}' and public key '{public}' do not match")]
- KeyMismatch { secret: PathBuf, public: PathBuf },
-}
-
-/// An in-memory signer that keeps its secret key internally
-/// so that signing never fails.
-///
-/// Can be created from a [`Keystore`] with the [`MemorySigner::load`] function.
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct MemorySigner {
- public: PublicKey,
- secret: Zeroizing<SecretKey>,
-}
-
-impl signature::Signer<Signature> for MemorySigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
- Ok(Signature::from(self.secret.deref().deref().sign(msg, None)))
- }
-}
-
-impl signature::Signer<ExtendedSignature> for MemorySigner {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
- use signature::Keypair as _;
- Ok(ExtendedSignature {
- key: self.verifying_key(),
- sig: self.try_sign(msg)?,
- })
- }
-}
-
-impl AsRef<PublicKey> for MemorySigner {
- fn as_ref(&self) -> &PublicKey {
- &self.public
- }
-}
-
-impl signature::KeypairRef for MemorySigner {
- type VerifyingKey = PublicKey;
-}
-
-#[cfg(feature = "cyphernet")]
-impl EcSk for MemorySigner {
- type Pk = PublicKey;
-
- fn generate_keypair() -> (Self, Self::Pk)
- where
- Self: Sized,
- {
- let ms = Self::r#gen();
- let pk = ms.public;
-
- (ms, pk)
- }
-
- fn to_pk(&self) -> Result<Self::Pk, EcSkInvalid> {
- Ok(self.public)
- }
-}
-
-#[cfg(feature = "cyphernet")]
-impl Ecdh for MemorySigner {
- type SharedSecret = [u8; 32];
-
- fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
- self.secret.ecdh(pk).map_err(cyphernet::EcdhError::from)
- }
-}
-
-impl MemorySigner {
- /// Load this signer from a keystore, given a secret key passphrase.
- pub fn load(
- keystore: &Keystore,
- passphrase: Option<Passphrase>,
- ) -> Result<Self, MemorySignerError> {
- let secret = keystore
- .secret_key(passphrase)
- .map_err(|e| {
- if e.is_crypto_err() {
- MemorySignerError::InvalidPassphrase
- } else {
- e.into()
- }
- })?
- .ok_or_else(|| MemorySignerError::NotFound(keystore.secret_key_path().to_path_buf()))?;
-
- let Some(public_path) = keystore.public_key_path() else {
- // There is no public key in the key store, so there's nothing
- // to validate. Derive it from the secret key.
- return Ok(Self::from_secret(secret));
- };
-
- let public = keystore
- .public_key()?
- .ok_or_else(|| MemorySignerError::NotFound(public_path.to_path_buf()))?;
-
- secret
- .validate_public_key(&public.into())
- .map_err(|_| MemorySignerError::KeyMismatch {
- secret: keystore.secret_key_path().to_path_buf(),
- public: public_path.to_path_buf(),
- })?;
-
- Ok(Self { public, secret })
- }
-
- /// Create a new memory signer from the given secret key, deriving
- /// the public key from the secret key.
- pub fn from_secret(secret: Zeroizing<SecretKey>) -> Self {
- Self {
- public: secret.public_key().into(),
- secret,
- }
- }
-
- /// Box this signer into a trait object.
- pub fn boxed(self) -> Box<dyn Signer> {
- Box::new(self)
- }
-
- /// Generate a new memory signer.
- pub fn r#gen() -> Self {
- let keypair = KeyPair::generate();
- let sk = keypair.sk;
-
- Self {
- public: sk.public_key().into(),
- secret: Zeroizing::new(sk.into()),
- }
- }
-}
-
-impl TryFrom<ssh_key::PublicKey> for PublicKey {
- type Error = Error;
-
- fn try_from(public: ssh_key::PublicKey) -> Result<Self, Self::Error> {
- match public.key_data() {
- ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => {
- Ok(Self::from(*data))
- }
- _ => Err(Error::InvalidKeyType),
- }
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use std::borrow::ToOwned as _;
+
#[test]
fn test_init_passphrase() {
let tmp = tempfile::tempdir().unwrap();
let store = Keystore::new(&tmp);
let public = store
- .init(
- "test",
- Some("hunter".to_owned().into()),
- ec25519::Seed::default(),
- )
+ .init("test", Some("hunter".to_owned().into()), Seed::mock(1))
.unwrap();
assert_eq!(public, store.public_key().unwrap().unwrap());
assert!(store.is_encrypted().unwrap());
@@ -403,7 +254,10 @@ mod tests {
.secret_key(Some("hunter".to_owned().into()))
.unwrap()
.unwrap();
- assert_eq!(PublicKey::from(secret.public_key()), public);
+
+ let secret_public = secret.public_key();
+
+ assert_eq!(secret_public, &public);
store
.secret_key(Some("blunder".to_owned().into()))
@@ -415,12 +269,13 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let store = Keystore::new(&tmp);
- let public = store.init("test", None, ec25519::Seed::default()).unwrap();
+ let public = store.init("test", None, Seed::mock(1)).unwrap();
assert_eq!(public, store.public_key().unwrap().unwrap());
assert!(!store.is_encrypted().unwrap());
let secret = store.secret_key(None).unwrap().unwrap();
- assert_eq!(PublicKey::from(secret.public_key()), public);
+ let secret_public = secret.public_key();
+ assert_eq!(secret_public, &public);
}
#[test]
@@ -429,14 +284,10 @@ mod tests {
let store = Keystore::new(&tmp);
let public = store
- .init(
- "test",
- Some("hunter".to_owned().into()),
- ec25519::Seed::default(),
- )
+ .init("test", Some("hunter".to_owned().into()), Seed::mock(1))
.unwrap();
- let signer = MemorySigner::load(&store, Some("hunter".to_owned().into())).unwrap();
+ let signer = SigningKey::load(&store, Some("hunter".to_owned().into())).unwrap();
- assert_eq!(public, *signer.public_key());
+ assert_eq!(&public, signer.public_key());
}
}
diff --git a/crates/radicle-crypto/src/test.rs b/crates/radicle-crypto/src/test.rs
deleted file mode 100644
index cadc1f12a..000000000
--- a/crates/radicle-crypto/src/test.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod arbitrary;
-pub mod signer;
diff --git a/crates/radicle-crypto/src/test/arbitrary.rs b/crates/radicle-crypto/src/test/arbitrary.rs
deleted file mode 100644
index fdc072f78..000000000
--- a/crates/radicle-crypto/src/test/arbitrary.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use qcheck::Arbitrary;
-
-use crate::{KeyPair, PublicKey, SecretKey, Seed, test::signer::MockSigner};
-
-impl Arbitrary for MockSigner {
- fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let bytes: [u8; 32] = Arbitrary::arbitrary(g);
- let seed = Seed::new(bytes);
- let sk = KeyPair::from_seed(seed).sk;
-
- MockSigner::from(SecretKey::from(sk))
- }
-}
-
-impl Arbitrary for PublicKey {
- fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let bytes: [u8; 32] = Arbitrary::arbitrary(g);
- let seed = Seed::new(bytes);
- let keypair = KeyPair::from_seed(seed);
-
- keypair.pk.into()
- }
-}
diff --git a/crates/radicle-crypto/src/test/signer.rs b/crates/radicle-crypto/src/test/signer.rs
deleted file mode 100644
index 181eea1de..000000000
--- a/crates/radicle-crypto/src/test/signer.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-#[cfg(feature = "ssh")]
-use crate::ssh::ExtendedSignature;
-use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature};
-
-#[derive(Debug, Clone)]
-pub struct MockSigner {
- pk: PublicKey,
- sk: SecretKey,
-}
-
-#[cfg(feature = "ssh")]
-impl signature::Signer<ExtendedSignature> for MockSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
- use signature::Keypair as _;
- Ok(ExtendedSignature {
- key: self.verifying_key(),
- sig: self.try_sign(msg)?,
- })
- }
-}
-
-impl signature::Signer<Signature> for MockSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
- Ok(Signature(self.sk.sign(msg, None)))
- }
-}
-
-impl signature::Verifier<Signature> for MockSigner {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> {
- self.pk
- .verify(msg, signature)
- .map_err(signature::Error::from_source)
- }
-}
-
-impl AsRef<PublicKey> for MockSigner {
- fn as_ref(&self) -> &PublicKey {
- &self.pk
- }
-}
-
-impl signature::KeypairRef for MockSigner {
- type VerifyingKey = PublicKey;
-}
-
-impl MockSigner {
- pub fn new(rng: &mut fastrand::Rng) -> Self {
- let mut seed: [u8; 32] = [0; 32];
-
- for byte in &mut seed {
- *byte = rng.u8(..);
- }
- Self::from_seed(seed)
- }
-
- pub fn from_seed(seed: [u8; 32]) -> Self {
- let seed = Seed::new(seed);
- let keypair = KeyPair::from_seed(seed);
-
- Self::from(SecretKey::from(keypair.sk))
- }
-}
-
-impl From<SecretKey> for MockSigner {
- fn from(sk: SecretKey) -> Self {
- let pk = sk.public_key().into();
- Self { sk, pk }
- }
-}
-
-impl Default for MockSigner {
- fn default() -> Self {
- let seed = Seed::generate();
- let keypair = KeyPair::from_seed(seed);
- let sk = keypair.sk;
-
- Self {
- pk: sk.public_key().into(),
- sk: sk.into(),
- }
- }
-}
-
-impl PartialEq for MockSigner {
- fn eq(&self, other: &Self) -> bool {
- self.pk == other.pk
- }
-}
-
-impl Eq for MockSigner {}
-
-impl std::hash::Hash for MockSigner {
- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- self.pk.hash(state)
- }
-}
-
-#[cfg(feature = "cyphernet")]
-impl cyphernet::EcSk for MockSigner {
- type Pk = PublicKey;
-
- // TODO: Should be renamed to 'generate'.
- fn generate_keypair() -> (Self, Self::Pk)
- where
- Self: Sized,
- {
- let kp = Self::default();
- let pk = kp.pk;
-
- (kp, pk)
- }
-
- fn to_pk(&self) -> Result<Self::Pk, cyphernet::EcSkInvalid> {
- Ok(self.pk)
- }
-}
-
-#[cfg(feature = "cyphernet")]
-impl cyphernet::Ecdh for MockSigner {
- type SharedSecret = [u8; 32];
-
- fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
- self.sk.ecdh(pk).map_err(|_| cyphernet::EcdhError::WeakPk)
- }
-}
diff --git a/crates/radicle-node/src/fingerprint.rs b/crates/radicle-node/src/fingerprint.rs
index cd765a939..ef215340e 100644
--- a/crates/radicle-node/src/fingerprint.rs
+++ b/crates/radicle-node/src/fingerprint.rs
@@ -22,6 +22,7 @@
use thiserror::Error;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::profile::Home;
/// Fingerprint of a public key.
@@ -64,30 +65,24 @@ impl Fingerprint {
}
/// Initialize the fingerprint of the node with given public key.
- pub fn init(
- home: &Home,
- secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
- ) -> Result<(), Error> {
- let public_key = secret_key.deref().public_key().into();
+ pub fn init(home: &Home, secret_key: &crypto::SigningKey) -> Result<(), Error> {
+ let public_key = secret_key.public_key();
let mut file = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(path(home))?;
{
use std::io::Write as _;
- file.write_all(crypto::ssh::fmt::fingerprint(&public_key).as_ref())?;
+ file.write_all(crypto::ssh::fmt::fingerprint(public_key).as_ref())?;
}
Ok(())
}
/// Verify that the fingerprint of given public key matches self.
- pub fn verify(
- &self,
- secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
- ) -> FingerprintVerification {
- let public_key = secret_key.deref().public_key().into();
- if crypto::ssh::fmt::fingerprint(&public_key) == self.0 {
+ pub fn verify(&self, secret_key: &crypto::SigningKey) -> FingerprintVerification {
+ let public_key = secret_key.public_key();
+ if crypto::ssh::fmt::fingerprint(public_key) == self.0 {
FingerprintVerification::Match
} else {
FingerprintVerification::Mismatch
@@ -104,7 +99,7 @@ fn path(home: &Home) -> std::path::PathBuf {
mod tests {
use super::*;
- use crypto::ssh::Keystore;
+ use crypto::{Seed, ssh::Keystore};
#[test]
fn matching() {
@@ -112,7 +107,7 @@ mod tests {
let home = Home::new(tmp.path()).unwrap();
let store = Keystore::new(&home.keys());
- store.init("test 1", None, crypto::Seed::default()).unwrap();
+ store.init("test 1", None, Seed::mock(1)).unwrap();
let secret = store.secret_key(None).unwrap().unwrap();
assert_eq!(Fingerprint::read(&home).unwrap(), None);
@@ -124,7 +119,7 @@ mod tests {
// Generate a new keypair, which does not match the fingerprint.
// This simulates the user modifying `~/.radicle/keys`.
std::fs::remove_dir_all(home.keys()).unwrap();
- store.init("test 1", None, crypto::Seed::default()).unwrap();
+ store.init("test 1", None, Seed::mock(2)).unwrap();
let other_secret = store.secret_key(None).unwrap().unwrap();
assert_ne!(secret, other_secret);
diff --git a/crates/radicle-node/src/main.rs b/crates/radicle-node/src/main.rs
index c758f0477..38ec395d4 100644
--- a/crates/radicle-node/src/main.rs
+++ b/crates/radicle-node/src/main.rs
@@ -6,10 +6,11 @@ use std::str::FromStr;
use thiserror::Error;
-use radicle::node::device::Device;
+use radicle::crypto::Signer as _;
+use radicle::node::NodeId;
use radicle::profile;
-use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
+use radicle_node::crypto::ssh::keystore::Keystore;
use radicle_node::fingerprint::{Fingerprint, FingerprintVerification};
use radicle_node::{Runtime, VERSION};
use radicle_signals as signals;
@@ -313,7 +314,7 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
let keystore = Keystore::from_secret_path(&secret_path);
- let secret_key = keystore
+ let signer = keystore
.secret_key(passphrase.clone())
.map_err(|err| ExecutionError::SecretLoading {
path: secret_path.clone(),
@@ -325,7 +326,7 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
if let Some(fp) = Fingerprint::read(&home)? {
log::debug!(target: "node", "Verifying fingerprint..");
- if fp.verify(&secret_key) != FingerprintVerification::Match {
+ if fp.verify(&signer) != FingerprintVerification::Match {
return Err(ExecutionError::FingerprintMismatch {
secret: keystore.secret_key_path().to_path_buf(),
fingerprint: fp,
@@ -333,11 +334,10 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
}
} else {
log::info!(target: "node", "Initializing fingerprint..");
- Fingerprint::init(&home, &secret_key)?;
+ Fingerprint::init(&home, &signer)?;
}
- let signer = Device::from(MemorySigner::from_secret(secret_key));
- log::info!(target: "node", "Node ID is {}", signer.public_key());
+ log::info!(target: "node", "Node ID is {}", NodeId::from(*signer.public_key()));
// Add the preferred seeds as persistent peers so that we reconnect to them automatically.
config.node.connect.extend(config.preferred_seeds);
diff --git a/crates/radicle-node/src/runtime.rs b/crates/radicle-node/src/runtime.rs
index 6f142b704..8fd326597 100644
--- a/crates/radicle-node/src/runtime.rs
+++ b/crates/radicle-node/src/runtime.rs
@@ -12,13 +12,11 @@ use std::os::unix::net::UnixListener;
#[cfg(windows)]
use uds_windows::UnixListener;
-use cyphernet::Ecdh;
use radicle::cob::migrate;
-use radicle::crypto;
-use radicle::node::device::Device;
use radicle_signals::Signal;
use thiserror::Error;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::node;
use radicle::node::Event;
use radicle::node::UserAgent;
@@ -124,22 +122,15 @@ impl Runtime {
/// Initialize the runtime.
///
/// This function spawns threads.
- pub fn init<G>(
+ pub fn init(
home: Home,
config: radicle::node::Config,
socket: PathBuf,
listen: Vec<net::SocketAddr>,
signals: mpsc::Receiver<Signal>,
- signer: Device<G>,
- ) -> Result<Runtime, Error>
- where
- G: crypto::signature::Signer<crypto::Signature>
- + Ecdh<Pk = NodeId>
- + Clone
- + Debug
- + 'static,
- {
- let id = *signer.public_key();
+ secret_key: SigningKey,
+ ) -> Result<Runtime, Error> {
+ let id = NodeId::from(*secret_key.public_key());
let alias = config.alias.clone();
let network = config.network;
let rng = fastrand::Rng::new();
@@ -215,7 +206,7 @@ impl Runtime {
stores,
storage.clone(),
policies,
- signer.clone(),
+ secret_key.clone(),
rng,
announcement,
emitter.clone(),
@@ -224,7 +215,7 @@ impl Runtime {
let (worker_send, worker_recv) =
crossbeam_channel::bounded::<worker::Task>(MAX_PENDING_TASKS);
- let mut wire = Wire::new(service, worker_send, signer.clone());
+ let mut wire = Wire::new(service, worker_send, secret_key.clone());
let mut local_addrs = Vec::new();
for addr in listen {
@@ -237,7 +228,7 @@ impl Runtime {
let reactor = Reactor::new(wire, thread::name(&id, "service"))?;
let handle = Handle::new(home.clone(), socket.clone(), reactor.controller(), emitter);
- let nid = *signer.public_key();
+ let nid = NodeId::from(*secret_key.public_key());
let fetch = worker::FetchConfig {
local: nid,
expiry: worker::garbage::Expiry::default(),
diff --git a/crates/radicle-node/src/test/gossip.rs b/crates/radicle-node/src/test/gossip.rs
index 4a774a32f..69a23dbd6 100644
--- a/crates/radicle-node/src/test/gossip.rs
+++ b/crates/radicle-node/src/test/gossip.rs
@@ -1,6 +1,6 @@
+use radicle::crypto::SigningKey;
use radicle::node;
use radicle::node::UserAgent;
-use radicle::node::device::Device;
use radicle::test::fixtures::r#gen;
use crate::test::arbitrary;
@@ -15,7 +15,7 @@ pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Messa
let mut msgs = Vec::new();
for _ in 0..count {
- let signer = Device::mock_rng(&mut rng);
+ let signer = SigningKey::mock(rng.usize(0x1000..0xFFFF));
let time = if delta == LocalDuration::from_secs(0) {
now
} else {
diff --git a/crates/radicle-node/src/test/node.rs b/crates/radicle-node/src/test/node.rs
index 3591bc8a6..0545fc069 100644
--- a/crates/radicle-node/src/test/node.rs
+++ b/crates/radicle-node/src/test/node.rs
@@ -1,4 +1,3 @@
-use std::fmt::Debug;
use std::io::BufRead as _;
use std::mem::ManuallyDrop;
use std::net::Ipv4Addr;
@@ -11,16 +10,12 @@ use std::{
};
use crate::node::NodeId;
-use crate::node::device::Device;
use crate::storage::git::transport;
use crate::{Runtime, runtime, runtime::Handle, service};
use radicle::Storage;
use radicle::cob;
use radicle::cob::issue;
-use radicle::crypto::Signature;
-use radicle::crypto::signature::Signer;
-use radicle::crypto::ssh::keystore::MemorySigner;
-use radicle::crypto::test::signer::MockSigner;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::git;
use radicle::git::fmt::refname;
use radicle::identity::{RepoId, Visibility};
@@ -38,19 +33,19 @@ use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository a
use radicle::test::fixtures;
/// A node that can be run.
-pub struct Node<G> {
+pub struct Node {
pub id: NodeId,
pub home: Home,
- pub signer: Device<G>,
+ pub secret_key: SigningKey,
pub storage: Storage,
pub config: Config,
pub db: service::Stores<Database>,
pub policies: policy::Store<policy::Write>,
}
-impl Node<MemorySigner> {
+impl Node {
pub fn new(profile: Profile) -> Self {
- let signer = Device::from(MemorySigner::load(&profile.keystore, None).unwrap());
+ let secret_key = profile.keystore.secret_key(None).unwrap().unwrap();
let id = *profile.id();
let policies_db = profile.home.node().join(POLICIES_DB_FILE);
let policies = policy::Store::open(policies_db).unwrap();
@@ -61,7 +56,7 @@ impl Node<MemorySigner> {
id,
home: profile.home,
config: profile.config.node,
- signer,
+ secret_key,
db,
policies,
storage: profile.storage,
@@ -70,18 +65,18 @@ impl Node<MemorySigner> {
}
/// Handle to a running node.
-pub struct NodeHandle<G: 'static> {
+pub struct NodeHandle {
pub id: NodeId,
pub alias: Alias,
pub storage: Storage,
- pub signer: Device<G>,
+ pub signer: SigningKey,
pub home: Home,
pub addr: net::SocketAddr,
pub thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
pub handle: ManuallyDrop<Handle>,
}
-impl<G: 'static> Drop for NodeHandle<G> {
+impl Drop for NodeHandle {
fn drop(&mut self) {
log::debug!(target: "test", "Node {} shutting down..", self.id);
@@ -95,12 +90,12 @@ impl<G: 'static> Drop for NodeHandle<G> {
}
}
-impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
+impl NodeHandle {
/// Connect this node to another node, and wait for the connection to be established both ways.
///
/// If the remote has blocked this node, then the remote event will be
/// [`Event::PeerDisconnected`].
- pub fn connect(&mut self, remote: &NodeHandle<G>) -> &mut Self {
+ pub fn connect(&mut self, remote: &NodeHandle) -> &mut Self {
let local_events = self.handle.events();
let remote_events = remote.handle.events();
@@ -130,7 +125,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
self
}
- pub fn disconnect(&mut self, remote: &NodeHandle<G>) {
+ pub fn disconnect(&mut self, remote: &NodeHandle) {
self.handle.disconnect(remote.id).unwrap();
}
@@ -176,7 +171,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
/// Wait until this node's routing table matches the remotes.
pub fn converge<'a>(
&'a self,
- remotes: impl IntoIterator<Item = &'a NodeHandle<G>>,
+ remotes: impl IntoIterator<Item = &'a NodeHandle>,
) -> BTreeSet<(RepoId, NodeId)> {
converge(iter::once(self).chain(remotes))
}
@@ -381,7 +376,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
None => git::fmt::lit::refs_heads(refname).into(),
Some(refname) => refname,
};
- let refname = refname.with_namespace(self.id.to_component());
+ let refname = refname.with_namespace(git::fmt::Component::from(&self.id));
let repo = self.storage.repository(rid).unwrap();
let raw = &repo.backend;
@@ -436,21 +431,18 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
}
}
-impl Node<MockSigner> {
+impl Node {
/// Create a new node.
- pub fn init(base: &Path, config: Config) -> Self {
- let home = base.join(
- iter::repeat_with(fastrand::alphanumeric)
- .take(8)
- .collect::<String>(),
- );
+ pub fn init(base: &Path, config: Config, id: usize) -> Self {
+ let home = base.join(config.alias.to_string());
let home = Home::new(home).unwrap();
- let signer = Device::mock();
+ let secret_key = SigningKey::mock(id);
+ let nid = NodeId::from(*secret_key.public_key());
let storage = Storage::open(
home.storage(),
git::UserInfo {
alias: config.alias.clone(),
- key: *signer.public_key(),
+ key: nid,
},
)
.unwrap();
@@ -460,11 +452,11 @@ impl Node<MockSigner> {
.unwrap();
let db = service::Stores::from(db);
- log::debug!(target: "test", "Node::init {}: {}", config.alias, signer.public_key());
+ log::debug!(target: "test", "Node::init {}: {}", config.alias, nid);
Self {
- id: *signer.public_key(),
+ id: nid,
home,
- signer,
+ secret_key,
storage,
config,
db,
@@ -473,10 +465,11 @@ impl Node<MockSigner> {
}
}
-impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G> {
+impl Node {
/// Spawn a node in its own thread.
- pub fn spawn(self) -> NodeHandle<G> {
+ pub fn spawn(self) -> NodeHandle {
let alias = self.config.alias.clone();
+
let listen = vec![(Ipv4Addr::LOCALHOST, 0).into()];
let (_, signals) = mpsc::sync_channel(1);
let rt = Runtime::init(
@@ -485,11 +478,12 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
self.home.socket_default(),
listen,
signals,
- self.signer.clone(),
+ self.secret_key.clone(),
)
.unwrap();
+
let addr = *rt.local_addrs.first().unwrap();
- let id = *self.signer.public_key();
+ let id = NodeId::from(*self.secret_key.public_key());
let handle = ManuallyDrop::new(rt.handle.clone());
let thread = ManuallyDrop::new(runtime::thread::spawn(&id, "runtime", move || rt.run()));
@@ -497,7 +491,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
id,
alias,
storage: self.storage,
- signer: self.signer,
+ signer: self.secret_key,
home: self.home,
addr,
handle,
@@ -521,7 +515,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
description,
branch.clone(),
Visibility::default(),
- &self.signer,
+ &self.secret_key,
&self.storage,
)
.map(|(id, _, _)| id)
@@ -531,7 +525,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
log::debug!(
target: "test",
- "Initialized project {id} for node {}", self.signer.public_key()
+ "Initialized project {id} for node {}", NodeId::from(*self.secret_key.public_key())
);
// Push local branches to storage.
@@ -558,7 +552,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
self.storage
.repository(id)
.unwrap()
- .sign_refs(&self.signer)
+ .sign_refs(&self.secret_key)
.unwrap();
id
@@ -575,9 +569,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
/// Checks whether the nodes have converged in their routing tables.
#[track_caller]
-pub fn converge<'a, G: Signer<Signature> + cyphernet::Ecdh + 'static>(
- nodes: impl IntoIterator<Item = &'a NodeHandle<G>>,
-) -> BTreeSet<(RepoId, NodeId)> {
+pub fn converge<'a>(nodes: impl IntoIterator<Item = &'a NodeHandle>) -> BTreeSet<(RepoId, NodeId)> {
let nodes = nodes.into_iter().collect::<Vec<_>>();
let mut all_routes = BTreeSet::<(RepoId, NodeId)>::new();
diff --git a/crates/radicle-node/src/test/peer.rs b/crates/radicle-node/src/test/peer.rs
index 97fe7a3a5..b14fa238d 100644
--- a/crates/radicle-node/src/test/peer.rs
+++ b/crates/radicle-node/src/test/peer.rs
@@ -7,20 +7,18 @@ use std::str::FromStr;
use log::*;
use radicle::Storage;
-use radicle::crypto::Signer as _;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::git::Oid;
use radicle::identity::Visibility;
use radicle::node::Database;
use radicle::node::UserAgent;
use radicle::node::address::Store as _;
-use radicle::node::device::Device;
use radicle::node::{Alias, ConnectOptions, address};
use radicle::rad;
use radicle::storage::refs;
use radicle::storage::refs::{RefsAt, SignedRefs};
use radicle::storage::{ReadRepository, RemoteRepository};
-use crate::crypto::test::signer::MockSigner;
use crate::identity::RepoId;
use crate::node;
use crate::node::routing::Store as _;
@@ -42,7 +40,7 @@ use radicle::node::policy::{Scope, SeedingPolicy};
use radicle_protocol::bounded::BoundedVec;
/// Service instantiation used for testing.
-pub type Service<S> = service::Service<Database, S, MockSigner>;
+pub type Service<S> = service::Service<Database, S>;
pub const AMY: u8 = 0x0A;
pub const BOB: u8 = 0x0B;
@@ -124,24 +122,18 @@ pub struct Config {
pub(crate) config: radicle::node::Config,
pub(crate) local_time: LocalTime,
pub(crate) policy: SeedingPolicy,
- pub(crate) signer: MockSigner,
+ pub(crate) secret_key: SigningKey,
}
impl Config {
pub(crate) fn new(id: usize) -> Self {
let config = radicle::node::Config::test(Alias::from_str("mocky").unwrap());
- let seed = std::iter::repeat_n(id, 32 / std::mem::size_of::<usize>())
- .flat_map(|i| i.to_le_bytes())
- .collect::<Vec<_>>()
- .try_into()
- .unwrap();
-
Config {
config,
local_time: LocalTime::now(),
policy: SeedingPolicy::default(),
- signer: MockSigner::from_seed(seed),
+ secret_key: SigningKey::mock(id),
}
}
}
@@ -156,7 +148,7 @@ impl Peer<Storage> {
description,
radicle::git::fmt::refname!("master"),
Visibility::default(),
- self.signer(),
+ self.secret_key(),
self.storage(),
)
.unwrap();
@@ -198,7 +190,7 @@ where
let tempdir = tempfile::TempDir::with_prefix(name).unwrap();
- let nid = *config.signer.public_key();
+ let nid = *config.secret_key.public_key();
// Initialize database.
let db = Database::open(
@@ -226,7 +218,7 @@ where
db,
storage,
policies,
- Device::new(config.signer),
+ config.secret_key,
fastrand::Rng::with_seed(id as u64),
announcement,
emitter,
@@ -305,7 +297,7 @@ where
inventory: arbitrary::vec(3).try_into().unwrap(),
timestamp: Timestamp::from(*self.clock()),
},
- self.signer(),
+ self.secret_key(),
)
}
@@ -322,7 +314,7 @@ where
}
.solve(0)
.unwrap(),
- self.signer(),
+ self.secret_key(),
)
}
@@ -355,12 +347,12 @@ where
}
pub fn announcement(&self, ann: impl Into<AnnouncementMessage>) -> Message {
- ann.into().signed(self.signer()).into()
+ ann.into().signed(self.secret_key()).into()
}
pub fn signed_refs_at(&self, root: Oid) -> SignedRefs {
arbitrary::with_gen(8, |g| {
- refs::arbitrary::signed_refs_at(g, root, self.signer())
+ refs::arbitrary::signed_refs_at(g, root, self.secret_key())
})
}
diff --git a/crates/radicle-node/src/tests.rs b/crates/radicle-node/src/tests.rs
index 5ed13be4b..c046575e8 100644
--- a/crates/radicle-node/src/tests.rs
+++ b/crates/radicle-node/src/tests.rs
@@ -8,12 +8,11 @@ use std::sync::Arc;
use std::sync::LazyLock;
use std::time;
-use radicle::node::device::Device;
use radicle::storage::ReadRepository;
use test_log::test;
use radicle::cob;
-use radicle::crypto::test::signer::MockSigner;
+use radicle::crypto::SigningKey;
use radicle::identity::Visibility;
use radicle::node::Link;
use radicle::node::address::Store as _;
@@ -259,7 +258,7 @@ fn inventory_sync() {
Storage::open(tmp.path().join("amy"), fixtures::user()).unwrap(),
);
let bob_secret = BOB;
- let bob_signer = Device::mock_from_seed([bob_secret; 32]);
+ let bob_signer = SigningKey::mock(bob_secret as usize);
let bob_storage = fixtures::storage(tmp.path().join("bob"), &bob_signer).unwrap();
let bob = Peer::with_storage("bob", bob_secret, bob_storage);
let now = LocalTime::now().into();
@@ -273,7 +272,7 @@ fn inventory_sync() {
inventory: repos.clone().try_into().unwrap(),
timestamp: now,
},
- bob.signer(),
+ bob.secret_key(),
),
);
@@ -378,7 +377,7 @@ fn inventory_pruning() {
.unwrap(),
timestamp: bob.local_time().into(),
},
- peer.signer(),
+ peer.secret_key(),
),
);
}
@@ -434,7 +433,7 @@ fn inventory_relay_bad_timestamp() {
inventory: BoundedVec::new(),
timestamp,
},
- bob.signer(),
+ bob.secret_key(),
),
);
assert_matches!(
@@ -669,13 +668,9 @@ fn refs_announcement_relay_public() {
let bob = {
const ID: u8 = BOB;
- let secret_key = MockSigner::from_seed([ID; 32]);
- let storage =
- fixtures::storage(tmp.path().join("bob"), &Device::new(secret_key.clone())).unwrap();
-
- Peer::new_with("bob", ID, storage, move |config| {
- config.signer = secret_key;
- })
+ let secret_key = SigningKey::mock(ID as usize);
+ let storage = fixtures::storage(tmp.path().join("bob"), &secret_key).unwrap();
+ Peer::with_storage("bob", ID, storage)
};
let bob_inv = bob.inventory().into_iter().collect::<Vec<_>>();
@@ -741,10 +736,9 @@ fn refs_announcement_relay_private() {
);
let bob = {
- let signer = MockSigner::from_seed([BOB; 32]);
+ let signer = SigningKey::mock(BOB as usize);
- let storage =
- fixtures::storage(tmp.path().join("bob"), &Device::new(signer.clone())).unwrap();
+ let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
Peer::with_storage("bob", BOB, storage)
};
@@ -819,7 +813,7 @@ fn refs_announcement_fetch_trusted_no_inventory() {
let bob = {
let mut rng = fastrand::Rng::new();
let id = rng.u8(100..200);
- let secret_key = Device::mock_from_seed([id; 32]);
+ let secret_key = SigningKey::mock(id as usize);
let storage = fixtures::storage(tmp.path().join("bob"), &secret_key).unwrap();
Peer::with_storage("bob", id, storage)
@@ -917,13 +911,10 @@ fn refs_announcement_no_subscribe() {
fn refs_announcement_offline() {
let tmp = tempfile::tempdir().unwrap();
let mut amy = {
- const ID: u8 = AMY;
- let signer = MockSigner::from_seed([ID; 32]);
- let secret_key = Device::new(signer.clone());
+ let id = AMY;
+ let secret_key = SigningKey::mock(id as usize);
let storage = fixtures::storage(tmp.path().join("amy"), &secret_key).unwrap();
- Peer::new_with("amy", ID, storage, move |config| {
- config.signer = signer;
- })
+ Peer::with_storage("amy", id, storage)
};
let mut bob = Peer::bob();
@@ -955,7 +946,7 @@ fn refs_announcement_offline() {
// Create an issue without telling the node.
let repo = amy.storage().repository(rid).unwrap();
let old_refs = RefsAt::new(&repo, *amy.nid()).unwrap();
- let mut issues = radicle::issue::Cache::no_cache(&repo, amy.signer()).unwrap();
+ let mut issues = radicle::issue::Cache::no_cache(&repo, amy.secret_key()).unwrap();
issues
.create(
cob::Title::new("Issue while offline!").unwrap(),
@@ -1028,7 +1019,7 @@ fn inventory_relay() {
inventory: inv.clone(),
timestamp: now,
},
- bob.signer(),
+ bob.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1055,7 +1046,7 @@ fn inventory_relay() {
inventory: inv.clone(),
timestamp: now,
},
- bob.signer(),
+ bob.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1073,7 +1064,7 @@ fn inventory_relay() {
inventory: inv.clone(),
timestamp: now + 1,
},
- bob.signer(),
+ bob.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1097,7 +1088,7 @@ fn inventory_relay() {
inventory: inv,
timestamp: now,
},
- cid.signer(),
+ cid.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1383,7 +1374,7 @@ fn fetch_missing_inventory_on_gossip() {
inventory: vec![rid].try_into().unwrap(),
timestamp: now.into(),
},
- bob.signer(),
+ bob.secret_key(),
),
);
amy.outbox()
@@ -1407,7 +1398,7 @@ fn fetch_missing_inventory_on_schedule() {
inventory: vec![rid].try_into().unwrap(),
timestamp: now.into(),
},
- bob.signer(),
+ bob.secret_key(),
),
);
amy.fetched(
@@ -1680,7 +1671,7 @@ fn refs_synced_event() {
.unwrap(),
timestamp: Timestamp::from(*bob.clock()),
});
- let msg = ann.signed(bob.signer());
+ let msg = ann.signed(bob.secret_key());
amy.seed(&acme, policy::Scope::All).unwrap();
amy.connect_to(&bob);
@@ -1753,7 +1744,7 @@ fn init_and_seed() {
"amy's repo",
git::fmt::refname!("master"),
Visibility::default(),
- amy.signer(),
+ amy.secret_key(),
amy.storage(),
)
.unwrap();
diff --git a/crates/radicle-node/src/tests/e2e.rs b/crates/radicle-node/src/tests/e2e.rs
index 6ab9579d5..ef0eef878 100644
--- a/crates/radicle-node/src/tests/e2e.rs
+++ b/crates/radicle-node/src/tests/e2e.rs
@@ -3,12 +3,12 @@ use std::{collections::HashSet, thread, time};
use radicle::cob;
use radicle::cob::Title;
use radicle::cob::store::access::{ReadOnly, WriteAs};
-use radicle_crypto::test::signer::MockSigner;
+use radicle::crypto::{Signer as _, SigningKey};
+use radicle::git::fmt::Component;
use test_log::test;
use radicle::git::raw::ErrorExt as _;
use radicle::node::Event;
-use radicle::node::device::Device;
use radicle::node::policy::Scope;
use radicle::node::{Alias, ConnectResult, DEFAULT_TIMEOUT, FetchResult, Handle as _};
use radicle::storage::{
@@ -56,8 +56,8 @@ mod config {
fn test_inventory_sync_basic() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
alice.project("alice", "");
bob.project("bob", "");
@@ -78,9 +78,9 @@ fn test_inventory_sync_basic() {
fn test_inventory_sync_bridge() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let mut eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let mut eve = Node::init(tmp.path(), config::relay("eve"), 42);
alice.project("alice", "");
bob.project("bob", "");
@@ -106,10 +106,10 @@ fn test_inventory_sync_bridge() {
fn test_inventory_sync_ring() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let mut eve = Node::init(tmp.path(), config::relay("eve"));
- let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let mut eve = Node::init(tmp.path(), config::relay("eve"), 42);
+ let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")), 73);
alice.project("alice", "");
bob.project("bob", "");
@@ -141,11 +141,11 @@ fn test_inventory_sync_ring() {
fn test_inventory_sync_star() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let mut eve = Node::init(tmp.path(), config::relay("eve"));
- let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
- let mut dave = Node::init(tmp.path(), Config::test(Alias::new("dave")));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let mut eve = Node::init(tmp.path(), config::relay("eve"), 42);
+ let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")), 73);
+ let mut dave = Node::init(tmp.path(), Config::test(Alias::new("dave")), 91);
alice.project("alice", "");
bob.project("bob", "");
@@ -171,8 +171,8 @@ fn test_inventory_sync_star() {
#[test]
fn test_replication() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -247,8 +247,8 @@ fn test_replication() {
#[test]
fn test_replication_ref_in_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
// Delete one of the signed refs.
@@ -298,9 +298,9 @@ fn test_replication_ref_in_sigrefs() {
#[test]
fn test_replication_invalid() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let carol = Device::mock();
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let carol = SigningKey::mock(8);
let acme = bob.project("acme", "");
let repo = bob.storage.repository_mut(acme).unwrap();
let (_, head) = repo.head().unwrap();
@@ -309,7 +309,8 @@ fn test_replication_invalid() {
// Create some unsigned refs for Carol in Bob's storage.
repo.raw()
.reference(
- &git::fmt::qualified!("refs/heads/carol").with_namespace(carol.public_key().into()),
+ &git::fmt::qualified!("refs/heads/carol")
+ .with_namespace(Component::from(carol.public_key())),
head.into(),
true,
&String::default(),
@@ -352,8 +353,8 @@ fn test_replication_invalid() {
#[test]
fn test_migrated_clone() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
@@ -409,8 +410,8 @@ fn test_migrated_clone() {
#[test]
fn test_dont_fetch_owned_refs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
@@ -440,13 +441,13 @@ fn test_dont_fetch_owned_refs() {
#[test]
fn test_fetch_followed_remotes() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut signers = Vec::with_capacity(5);
{
- for _ in 0..5 {
- let signer = Device::mock();
+ for i in 0..5 {
+ let signer = SigningKey::mock(i);
rad::fork_remote(acme, &alice.id, &signer, &alice.storage).unwrap();
signers.push(signer);
}
@@ -496,13 +497,13 @@ fn test_fetch_followed_remotes() {
#[test]
fn test_missing_remote() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
let mut bob = bob.spawn();
- let carol = Device::mock();
+ let carol = SigningKey::mock(98);
alice.connect(&bob);
converge([&alice, &bob]);
@@ -534,8 +535,8 @@ fn test_missing_remote() {
#[test]
fn test_fetch_preserve_owned_refs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
let mut bob = bob.spawn();
@@ -584,8 +585,8 @@ fn test_fetch_preserve_owned_refs() {
#[test]
fn test_clone() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -645,8 +646,8 @@ fn test_clone() {
#[test]
fn test_fetch_up_to_date() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -678,8 +679,8 @@ fn test_fetch_up_to_date() {
#[test]
fn test_fetch_unseeded() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -712,8 +713,8 @@ fn test_fetch_unseeded() {
fn test_large_fetch() {
let tmp = tempfile::tempdir().unwrap();
let scale = config::scale();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let (repo, _) = fixtures::repository(tmp.path());
fixtures::populate(&repo, scale.max(3));
@@ -763,6 +764,7 @@ fn test_concurrent_fetches() {
relay: radicle::node::config::Relay::Always,
..config::relay("alice")
},
+ 13,
);
let mut bob = Node::init(
tmp.path(),
@@ -771,6 +773,7 @@ fn test_concurrent_fetches() {
relay: radicle::node::config::Relay::Always,
..config::relay("bob")
},
+ 37,
);
for i in 0..repos {
@@ -864,8 +867,8 @@ fn test_concurrent_fetches() {
#[test]
fn test_connection_crossing() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let alice = alice.spawn();
let bob = bob.spawn();
@@ -946,9 +949,9 @@ fn test_connection_crossing() {
fn test_non_fast_forward_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = bob.project("acme", "");
@@ -1060,9 +1063,9 @@ fn test_non_fast_forward_sigrefs() {
fn test_outdated_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = alice.project("acme", "");
@@ -1169,9 +1172,9 @@ fn test_outdated_sigrefs() {
fn test_outdated_delegate_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = alice.project("acme", "");
@@ -1268,8 +1271,8 @@ fn test_outdated_delegate_sigrefs() {
fn missing_default_branch() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let rid = alice.project("acme", "");
@@ -1334,9 +1337,9 @@ fn missing_delegate_default_branch() {
use radicle::storage::git::Repository;
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let seed = Node::init(tmp.path(), config::relay("seed"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let seed = Node::init(tmp.path(), config::relay("seed"), 7);
let rid = alice.project("acme", "");
@@ -1401,7 +1404,7 @@ fn missing_delegate_default_branch() {
let new = repo.identity_doc().unwrap().doc;
assert!(
- new.is_delegate(&bob.signer.public_key().into()),
+ new.is_delegate(&bob_key.into()),
"Bob must be a delegate after the update"
);
}
@@ -1467,9 +1470,9 @@ fn missing_delegate_default_branch() {
fn test_background_foreground_fetch() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = alice.project("acme", "");
@@ -1563,10 +1566,10 @@ fn test_background_foreground_fetch() {
/// she is made aware of the changes via the `subscribe` message, and fetches from the seed.
fn test_catchup_on_refs_announcements() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let bob_id = bob.id;
- let seed = Node::init(tmp.path(), config::relay("seed"));
+ let seed = Node::init(tmp.path(), config::relay("seed"), 7);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
@@ -1600,8 +1603,8 @@ fn test_catchup_on_refs_announcements() {
fn test_multiple_offline_inits() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let radcliffe = alice.project("radcliffe", "");
@@ -1625,7 +1628,7 @@ fn test_multiple_offline_inits() {
#[test]
fn test_channel_reader_limit() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
let limits = radicle::node::config::Limits {
fetch_pack_receive: radicle::node::config::FetchPackSizeLimit::bytes(1000),
..radicle::node::config::Limits::default()
@@ -1636,6 +1639,7 @@ fn test_channel_reader_limit() {
limits,
..config::relay("bob")
},
+ 37,
);
let acme = alice.project("acme", "");
@@ -1670,8 +1674,8 @@ fn test_channel_reader_limit() {
fn test_fetch_emits_canonical_ref_update() {
let tmp = tempfile::tempdir().unwrap();
let scale = config::scale();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let (repo, _) = fixtures::repository(tmp.path());
fixtures::populate(&repo, scale.max(3));
@@ -1722,10 +1726,10 @@ fn test_non_fast_forward_identity_doc() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
- let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
- let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
- let alice_laptop = Node::init(tmp.path(), Config::test(Alias::new("alice-laptop")));
+ let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")), 13);
+ let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")), 37);
+ let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")), 42);
+ let alice_laptop = Node::init(tmp.path(), Config::test(Alias::new("alice-laptop")), 113);
let rid = alice.project("acme", "");
@@ -1735,7 +1739,7 @@ fn test_non_fast_forward_identity_doc() {
let bob_events = bob.handle.events();
let mut eve = eve.spawn();
- let has_issue = |node: &NodeHandle<MockSigner>, issue: &cob::ObjectId| -> bool {
+ let has_issue = |node: &NodeHandle, issue: &cob::ObjectId| -> bool {
let repo = node.storage.repository(rid).unwrap();
repo.contains(**issue).unwrap()
};
@@ -1837,8 +1841,8 @@ fn test_non_fast_forward_identity_doc() {
#[test]
fn test_block_active_connection() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let mut alice = alice.spawn();
let bob = bob.spawn();
@@ -1863,8 +1867,8 @@ fn test_block_active_connection() {
#[test]
fn test_block_prevents_connection() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let mut alice = alice.spawn();
let mut bob = bob.spawn();
@@ -1896,8 +1900,8 @@ fn test_block_prevents_connection() {
#[test]
fn test_block_prevents_fetch() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let rid = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -1919,8 +1923,8 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let rid = alice.project("acme", "");
@@ -1970,8 +1974,8 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
fn test_fetch_emits_canonical_ref_update_partial_glob() {
let tmp = tempfile::tempdir().unwrap();
let scale = config::scale();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let (repo, _) = fixtures::repository(tmp.path());
let rid = alice.project_from("acme", "", &repo);
diff --git a/crates/radicle-node/src/wire.rs b/crates/radicle-node/src/wire.rs
index 2691626fd..5b2f884a0 100644
--- a/crates/radicle-node/src/wire.rs
+++ b/crates/radicle-node/src/wire.rs
@@ -12,13 +12,12 @@ use crossbeam_channel as chan;
use cyphernet::addr::{HostName, InetHost, NetAddr};
use cyphernet::encrypt::noise::{HandshakePattern, Keyset, NoiseState};
use cyphernet::proxy::socks5;
-use cyphernet::{Digest, EcSk, Ecdh, Sha256};
+use cyphernet::{Digest, Sha256};
use localtime::LocalTime;
use mio::net::TcpStream;
-use radicle::node::device::Device;
use radicle::collections::{RandomMap, RandomSet};
-use radicle::crypto;
+use radicle::crypto::{self, Signer as _};
use radicle::node::Link;
use radicle::node::NodeId;
#[cfg(any(feature = "i2p", feature = "tor"))]
@@ -67,10 +66,10 @@ pub enum Control {
}
/// Peer session type.
-type WireSession<G> = NoiseSession<G, Sha256, Socks5Session<TcpStream>>;
+type WireSession = NoiseSession<crypto::SigningKey, Sha256, Socks5Session<TcpStream>>;
/// Reactor action.
-type Action<G> = reactor::Action<Listener, Transport<WireSession<G>>>;
+type Action = reactor::Action<Listener, Transport<WireSession>>;
/// A worker stream.
struct Stream {
@@ -294,17 +293,17 @@ impl Peers {
}
/// Wire protocol implementation for a set of peers.
-pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + Ecdh> {
+pub(crate) struct Wire<D, S> {
/// Backing service instance.
- service: Service<D, S, G>,
+ service: Service<D, S>,
/// Worker pool interface.
worker: chan::Sender<Task>,
/// Used for authentication.
- signer: Device<G>,
+ secret_key: radicle::crypto::SigningKey,
/// Node metrics.
metrics: service::Metrics,
/// Internal queue of actions to send to the reactor.
- actions: VecDeque<Action<G>>,
+ actions: VecDeque<Action>,
/// Outbound attempted peers without a session.
outbound: RandomMap<Token, Outbound>,
/// Inbound peers without a session.
@@ -317,19 +316,22 @@ pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + E
tokens: Tokens,
}
-impl<D, S, G> Wire<D, S, G>
+impl<D, S> Wire<D, S>
where
D: service::Store,
S: WriteStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId>,
{
- pub fn new(service: Service<D, S, G>, worker: chan::Sender<Task>, signer: Device<G>) -> Self {
+ pub fn new(
+ service: Service<D, S>,
+ worker: chan::Sender<Task>,
+ secret_key: crypto::SigningKey,
+ ) -> Self {
assert!(service.started().is_some(), "Service must be initialized");
Self {
service,
worker,
- signer,
+ secret_key,
metrics: Metrics::default(),
actions: VecDeque::new(),
inbound: RandomSet::default(),
@@ -483,14 +485,13 @@ where
}
}
-impl<D, S, G> reactor::ReactionHandler for Wire<D, S, G>
+impl<D, S> reactor::ReactionHandler for Wire<D, S>
where
D: service::Store + Send,
S: WriteStorage + Send + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + Send + Debug,
{
type Listener = Listener;
- type Transport = Transport<WireSession<G>>;
+ type Transport = Transport<WireSession>;
fn tick(&mut self) {
self.metrics.open_channels = self
@@ -539,11 +540,7 @@ where
return;
}
- let session = accept::<G>(
- remote.clone().into(),
- connection,
- self.signer.clone().into_inner(),
- );
+ let session = accept(remote.clone().into(), connection, self.secret_key.clone());
let transport = match Transport::with_session(session) {
Ok(transport) => transport,
Err(err) => {
@@ -581,13 +578,13 @@ where
}
}
- fn transport_reacted(&mut self, token: Token, event: SessionEvent<WireSession<G>>, _: Instant) {
+ fn transport_reacted(&mut self, token: Token, event: SessionEvent<WireSession>, _: Instant) {
match event {
SessionEvent::Established(ProtocolArtifact { state, session }) => {
// SAFETY: With the NoiseXK protocol, there is always a remote static key.
- let nid: NodeId = state.remote_static_key.unwrap();
+ let nid: NodeId = NodeId::from(*state.remote_static_key.unwrap().public_key());
// Make sure we don't try to connect to ourselves by mistake.
- if &nid == self.signer.public_key() {
+ if &nid == self.secret_key.public_key() {
log::warn!(target: "wire", "Self-connection detected, disconnecting..");
self.disconnect(token, DisconnectReason::SelfConnection);
@@ -629,7 +626,7 @@ where
use Precedence::*;
// Whether we have precedence in case of conflicting connections.
- let precedence = if *self.signer.public_key() > nid {
+ let precedence = if NodeId::from(*self.secret_key.public_key()) > nid {
Ours
} else {
Theirs
@@ -828,7 +825,7 @@ where
}
}
- fn handle_error(&mut self, err: reactor::Error<Listener, Transport<WireSession<G>>>) {
+ fn handle_error(&mut self, err: reactor::Error<Listener, Transport<WireSession>>) {
match err {
reactor::Error::Poll(err) | reactor::Error::Registration(err) => {
// TODO: This should be a fatal error, there's nothing we can do here.
@@ -910,13 +907,12 @@ where
}
}
-impl<D, S, G> Iterator for Wire<D, S, G>
+impl<D, S> Iterator for Wire<D, S>
where
D: service::Store,
S: WriteStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone,
{
- type Item = Action<G>;
+ type Item = Action;
fn next(&mut self) -> Option<Self::Item> {
while let Some(ev) = self.service.next() {
@@ -962,13 +958,13 @@ where
self.service.attempted(node_id, addr.clone());
self.metrics.peer(node_id).outbound_connection_attempts += 1;
- match dial::<G>(
+ match dial(
(*addr).clone(),
node_id,
- self.signer.clone().into_inner(),
+ self.secret_key.clone(),
self.service.config(),
)
- .and_then(Transport::<WireSession<G>>::with_session)
+ .and_then(Transport::<WireSession>::with_session)
{
Ok(transport) => {
let token = self.tokens.advance();
@@ -1085,12 +1081,16 @@ where
}
/// Establish a new outgoing connection.
-pub fn dial<G: Ecdh<Pk = NodeId>>(
+pub fn dial(
remote_addr: NetAddr<HostName>,
- remote_id: <G as EcSk>::Pk,
- signer: G,
+ remote_id: crypto::PublicKey,
+ signer: crypto::SigningKey,
config: &radicle::node::Config,
-) -> io::Result<WireSession<G>> {
+) -> io::Result<WireSession> {
+ // TODO: Nicer error.
+ let remote_id = crypto::VerifyingKey::try_from(&remote_id)
+ .map_err(|source| io::Error::new(io::ErrorKind::InvalidInput, source.to_string()))?;
+
#[cfg(any(feature = "i2p", feature = "tor"))]
fn proxy_or_forward<H: std::fmt::Display>(
config: &AddressConfig,
@@ -1157,7 +1157,7 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
// Whether to tunnel regular connections through the proxy.
let force_proxy = config.proxy.is_some();
- Ok(session::<G>(
+ Ok(session(
remote_addr,
Some(remote_id),
connection,
@@ -1167,22 +1167,22 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
}
/// Accept a new connection.
-pub fn accept<G: Ecdh<Pk = NodeId>>(
+pub fn accept(
remote_addr: NetAddr<HostName>,
connection: TcpStream,
- signer: G,
-) -> WireSession<G> {
- session::<G>(remote_addr, None, connection, false, signer)
+ secret_key: crypto::SigningKey,
+) -> WireSession {
+ session(remote_addr, None, connection, false, secret_key)
}
/// Create a new [`WireSession`].
-fn session<G: Ecdh<Pk = NodeId>>(
+fn session(
remote_addr: NetAddr<HostName>,
- remote_id: Option<NodeId>,
+ remote_id: Option<crypto::VerifyingKey>,
connection: TcpStream,
force_proxy: bool,
- signer: G,
-) -> WireSession<G> {
+ secret_key: crypto::SigningKey,
+) -> WireSession {
if let Err(e) = connection.set_nodelay(true) {
log::warn!(target: "wire", "Unable to set TCP_NODELAY on socket {connection:?}: {e}");
}
@@ -1254,11 +1254,9 @@ fn session<G: Ecdh<Pk = NodeId>>(
};
let noise = {
- let pair = G::generate_keypair();
-
let keyset = Keyset {
- e: pair.0,
- s: Some(signer),
+ e: crypto::SigningKey::from_seed(crypto::Seed::new([91; 32])),
+ s: Some(secret_key),
re: None,
rs: remote_id,
};
@@ -1287,7 +1285,6 @@ mod logger {
#[cfg(test)]
mod test {
use super::*;
- use crate::crypto::test::signer::MockSigner;
use radicle_protocol::service::ServiceState as _;
@@ -1428,7 +1425,7 @@ mod test {
// Returns the wire, the repo id, and bob's id/address.
#[allow(clippy::type_complexity)]
fn wire_with_active_fetch() -> (
- Wire<crate::node::Database, MockStorage, MockSigner>,
+ Wire<crate::node::Database, MockStorage>,
RepoId,
NodeId,
NetAddr<HostName>,
@@ -1457,7 +1454,11 @@ mod test {
assert!(alice.fetcher().active_fetches().contains_key(&rid));
let (worker_tx, _worker_rx) = chan::unbounded::<Task>();
- let wire = Wire::new(alice.into_service(), worker_tx, Device::mock());
+ let wire = Wire::new(
+ alice.into_service(),
+ worker_tx,
+ crypto::SigningKey::mock(100),
+ );
(wire, rid, bob_id, bob_addr)
}
diff --git a/crates/radicle-protocol/src/fetcher/test/state/multinode.rs b/crates/radicle-protocol/src/fetcher/test/state/multinode.rs
index fb5c3c4bc..8dedccd3c 100644
--- a/crates/radicle-protocol/src/fetcher/test/state/multinode.rs
+++ b/crates/radicle-protocol/src/fetcher/test/state/multinode.rs
@@ -1,4 +1,6 @@
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::test::arbitrary;
+
use radicle_core::{NodeId, RepoId};
use crate::fetcher::state::{command, event};
@@ -62,11 +64,13 @@ fn independent_queues() {
#[test]
fn high_count() {
+ const N: usize = 100;
+
let mut state = FetcherState::new(helpers::config(1, 10));
let config = FetchConfig::default();
- for i in 0..100 {
- let node: NodeId = arbitrary::r#gen(i + 1);
+ for i in 0..N {
+ let node: NodeId = *SigningKey::mock(i * 20).public_key();
let repo: RepoId = arbitrary::r#gen(i + 1);
let event = state.fetch(command::Fetch {
from: node,
@@ -77,5 +81,5 @@ fn high_count() {
assert!(matches!(event, event::Fetch::Started { .. }));
}
- assert_eq!(state.active_fetches().len(), 100);
+ assert_eq!(state.active_fetches().len(), N);
}
diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs
index 8530de5f8..89299ac70 100644
--- a/crates/radicle-protocol/src/service.rs
+++ b/crates/radicle-protocol/src/service.rs
@@ -29,7 +29,6 @@ use radicle::node::address;
use radicle::node::address::Store as _;
use radicle::node::address::{AddressBook, AddressType, KnownAddress};
use radicle::node::config::{PeerConfig, RateLimit};
-use radicle::node::device::Device;
use radicle::node::refs::Store as _;
use radicle::node::routing::Store as _;
use radicle::node::seed;
@@ -59,6 +58,7 @@ use radicle::storage::{Namespaces, ReadStorage, refs::RefsAt};
// use radicle::worker::fetch;
// use crate::worker::FetchError;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::node::Link;
use radicle::node::PROTOCOL_VERSION;
@@ -324,11 +324,11 @@ impl<D> From<D> for Stores<D> {
/// The node service.
#[derive(Debug)]
-pub struct Service<D, S, G> {
+pub struct Service<D, S> {
/// Service configuration.
config: Config,
/// Our cryptographic signer and key.
- signer: Device<G>,
+ secret_key: crypto::SigningKey,
/// Project storage.
storage: S,
/// Node database.
@@ -381,12 +381,7 @@ pub struct Service<D, S, G> {
metrics: Metrics,
}
-impl<D, S, G> Service<D, S, G> {
- /// Get the local node id.
- pub fn node_id(&self) -> NodeId {
- *self.signer.public_key()
- }
-
+impl<D, S> Service<D, S> {
/// Get the local service time.
pub fn local_time(&self) -> LocalTime {
self.clock
@@ -397,18 +392,17 @@ impl<D, S, G> Service<D, S, G> {
}
}
-impl<D, S, G> Service<D, S, G>
+impl<D, S> Service<D, S>
where
D: Store,
S: WriteStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature>,
{
/// Initialize service with current time. Call this once.
pub fn initialize(&mut self, time: LocalTime) -> Result<(), Error> {
debug!(target: "service", "Init @{}", time.as_millis());
assert_ne!(time, LocalTime::default());
- let nid = self.node_id();
+ let nid = *self.nid();
self.clock = time;
self.started_at = Some(time);
@@ -533,7 +527,7 @@ where
let repo = self.storage.repository_mut(rid)?;
// NOTE: We assume to reach `FeatureLevel::LATEST` by signing refs.
- let refs = repo.force_sign_refs(&self.signer)?;
+ let refs = repo.force_sign_refs(&self.secret_key)?;
let repo = self.storage.repository(rid)?;
let synced_at = SyncedAt::new(refs.at, &repo)?;
@@ -545,18 +539,17 @@ where
}
}
-impl<D, S, G> Service<D, S, G>
+impl<D, S> Service<D, S>
where
D: Store,
S: ReadStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature>,
{
pub fn new(
config: Config,
db: Stores<D>,
storage: S,
policies: policy::Config<Write>,
- signer: Device<G>,
+ secret_key: crypto::SigningKey,
rng: Rng,
node: NodeAnnouncement,
emitter: Emitter<Event>,
@@ -579,7 +572,7 @@ where
config,
storage,
policies,
- signer,
+ secret_key,
rng,
inventory,
node,
@@ -679,9 +672,10 @@ where
&self.policies
}
- /// Get the local signer.
- pub fn signer(&self) -> &Device<G> {
- &self.signer
+ /// Get the local secret key.
+ #[cfg(any(test, feature = "test"))]
+ pub fn secret_key(&self) -> &radicle::crypto::SigningKey {
+ &self.secret_key
}
/// Subscriber to inner `Emitter` events.
@@ -1756,7 +1750,7 @@ where
remote: &NodeId,
message: Message,
) -> Result<(), session::Error> {
- let local = self.node_id();
+ let local = *self.nid();
let relay = self.config.is_relay();
let Some(peer) = self.sessions.get_mut(remote) else {
debug!(target: "service", "Session not found for {remote}");
@@ -1918,7 +1912,7 @@ where
},
};
// Remove our own remote, we don't want to fetch that.
- refs.want.retain(|r| r.remote != self.node_id());
+ refs.want.retain(|r| r.remote != *self.nid());
Ok(refs)
}
@@ -1955,8 +1949,8 @@ where
debug!(target: "service", "Subscribing to messages since timestamp {since}..");
vec![
- Message::node(self.node.clone(), &self.signer),
- Message::inventory(self.inventory.clone(), &self.signer),
+ Message::node(self.node.clone(), &self.secret_key),
+ Message::inventory(self.inventory.clone(), &self.secret_key),
Message::subscribe(filter, since, Timestamp::MAX),
]
}
@@ -1972,7 +1966,7 @@ where
/// Remove a local repository from our inventory.
fn remove_inventory(&mut self, rid: &RepoId) -> Result<bool, Error> {
- let node = self.node_id();
+ let node = *self.nid();
let now = self.timestamp();
let removed = self.db.routing_mut().remove_inventory(rid, &node)?;
@@ -1984,7 +1978,7 @@ where
/// Add a local repository to our inventory.
fn add_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
- let node = self.node_id();
+ let node = *self.nid();
let now = self.timestamp();
if !self.storage.contains(&rid)? {
@@ -2119,7 +2113,7 @@ where
refs: refs.clone(),
timestamp,
});
- Ok((msg.signed(&self.signer), refs.into()))
+ Ok((msg.signed(&self.secret_key), refs.into()))
}
/// Announce our own refs for the given repo.
@@ -2208,7 +2202,7 @@ where
fn connect(&mut self, nid: NodeId, addr: Address) -> Result<(), ConnectError> {
debug!(target: "service", "Connecting to {nid} ({addr})..");
- if nid == self.node_id() {
+ if nid == *self.nid() {
return Err(ConnectError::SelfConnection);
}
if let Ok(true) = self.policies.is_blocked(&nid) {
@@ -2358,7 +2352,7 @@ where
fn relay_announcements(&mut self) -> Result<(), Error> {
let now = self.clock.into();
let rows = self.database_mut().gossip_mut().relays(now)?;
- let local = self.node_id();
+ let local = *self.nid();
for (id, msg) in rows {
let announcer = msg.node;
@@ -2382,7 +2376,7 @@ where
let msg = AnnouncementMessage::from(self.inventory.clone());
self.outbox.announce(
- msg.signed(&self.signer),
+ msg.signed(&self.secret_key),
self.sessions.connected().map(|(_, p)| p),
self.db.gossip_mut(),
);
@@ -2396,7 +2390,7 @@ where
}
let delta = count - usize::from(self.config.limits.routing_max_size);
- let nid = self.node_id();
+ let nid = *self.nid();
self.db.routing_mut().prune(
(*now - LocalDuration::from(self.config.limits.routing_max_age)).into(),
Some(delta),
@@ -2501,7 +2495,7 @@ where
continue;
}
}
- match self.seeds(&rid, [self.node_id()].into()) {
+ match self.seeds(&rid, [*self.nid()].into()) {
Ok(seeds) => {
if let Some(connected) = NonEmpty::from_vec(seeds.connected().collect()) {
for seed in connected {
@@ -2695,14 +2689,13 @@ pub trait ServiceState {
fn metrics(&self) -> &Metrics;
}
-impl<D, S, G> ServiceState for Service<D, S, G>
+impl<D, S> ServiceState for Service<D, S>
where
D: routing::Store,
- G: crypto::signature::Signer<crypto::Signature>,
S: ReadStorage,
{
fn nid(&self) -> &NodeId {
- self.signer.public_key()
+ self.secret_key.public_key()
}
fn sessions(&self) -> &Sessions {
diff --git a/crates/radicle-protocol/src/service/gossip/store.rs b/crates/radicle-protocol/src/service/gossip/store.rs
index 914b1b564..27762a926 100644
--- a/crates/radicle-protocol/src/service/gossip/store.rs
+++ b/crates/radicle-protocol/src/service/gossip/store.rs
@@ -130,7 +130,7 @@ impl Store for Database {
stmt.bind((4, &msg.encode_to_vec()[..]))?;
}
}
- stmt.bind((5, &ann.signature))?;
+ stmt.bind((5, sql::Value::Binary(ann.signature.to_vec())))?;
stmt.bind((6, &ann.message.timestamp()))?;
if let Some(row) = stmt.into_iter().next() {
@@ -341,7 +341,7 @@ impl TryFrom<&sql::Value> for GossipType {
mod parse {
use super::*;
- pub fn announcement(row: sql::Row) -> Result<(AnnouncementId, Announcement), Error> {
+ pub fn announcement(mut row: sql::Row) -> Result<(AnnouncementId, Announcement), Error> {
let id = row.try_read::<i64, _>("rowid")? as AnnouncementId;
let node = row.try_read::<NodeId, _>("node")?;
let gt = row.try_read::<GossipType, _>("type")?;
@@ -359,7 +359,25 @@ mod parse {
AnnouncementMessage::Node(ann)
}
};
- let signature = row.try_read::<Signature, _>("signature")?;
+
+ let signature = match row.take("signature") {
+ sql::Value::Binary(bytes) => {
+ Signature::try_from(bytes.as_slice()).map_err(|source| sql::Error {
+ code: None,
+ message: Some(format!("invalid signature: {source}")),
+ })?
+ }
+ value => {
+ return Err(sql::Error {
+ code: None,
+ message: Some(format!(
+ "invalid value of type '{:?}' for row signature",
+ value.kind()
+ )),
+ })?;
+ }
+ };
+
let timestamp = row.try_read::<Timestamp, _>("timestamp")?;
debug_assert_eq!(timestamp, message.timestamp());
@@ -382,8 +400,8 @@ mod test {
use crate::bounded::BoundedVec;
use localtime::LocalTime;
use radicle::assert_matches;
+ use radicle::crypto::SigningKey;
use radicle::identity::RepoId;
- use radicle::node::device::Device;
use radicle::test::arbitrary;
#[test]
@@ -392,18 +410,18 @@ mod test {
let nid = arbitrary::r#gen::<NodeId>(1);
let rid = arbitrary::r#gen::<RepoId>(1);
let timestamp = LocalTime::now().into();
- let signer = Device::mock();
+ let secret_key = SigningKey::mock(100);
let refs = AnnouncementMessage::Refs(RefsAnnouncement {
rid,
refs: BoundedVec::new(),
timestamp,
})
- .signed(&signer);
+ .signed(&secret_key);
let inv = AnnouncementMessage::Inventory(InventoryAnnouncement {
inventory: BoundedVec::new(),
timestamp,
})
- .signed(&signer);
+ .signed(&secret_key);
// Only the first announcement of each type is recognized as new.
let id1 = db.announced(&nid, &refs).unwrap().unwrap();
diff --git a/crates/radicle-protocol/src/service/message.rs b/crates/radicle-protocol/src/service/message.rs
index 945fd0e6e..705b27abc 100644
--- a/crates/radicle-protocol/src/service/message.rs
+++ b/crates/radicle-protocol/src/service/message.rs
@@ -5,10 +5,10 @@ use bytes::{Buf, BufMut};
use nonempty::NonEmpty;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::git;
use radicle::identity::RepoId;
use radicle::node;
-use radicle::node::device::Device;
use radicle::node::{Address, Alias, UserAgent};
use radicle::storage;
use radicle::storage::refs::RefsAt;
@@ -261,18 +261,15 @@ pub enum AnnouncementMessage {
impl AnnouncementMessage {
/// Sign this announcement message.
- pub fn signed<G>(self, signer: &Device<G>) -> Announcement
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ pub fn signed(self, secret_key: &crypto::SigningKey) -> Announcement {
use crypto::signature::Signer as _;
let msg = self.encode_to_vec();
- let signature = signer.sign(&msg);
+ let signature = secret_key.sign(&msg);
Announcement {
- node: *signer.public_key(),
+ node: *secret_key.public_key(),
message: self,
signature,
}
@@ -368,8 +365,15 @@ impl Announcement {
/// Verify this announcement's signature.
pub fn verify(&self) -> bool {
+ use crypto::signature::Verifier as _;
+
+ let Ok(verifier) = crypto::VerifyingKey::try_from(&self.node) else {
+ // Public key is not a valid verifying key, so the signature cannot be valid.
+ return false;
+ };
+
let msg = self.message.encode_to_vec();
- self.node.verify(msg, &self.signature).is_ok()
+ verifier.verify(&msg, &self.signature).is_ok()
}
pub fn matches(&self, filter: &Filter) -> bool {
@@ -449,18 +453,12 @@ impl Message {
.into()
}
- pub fn node<G: crypto::signature::Signer<crypto::Signature>>(
- message: NodeAnnouncement,
- signer: &Device<G>,
- ) -> Self {
- AnnouncementMessage::from(message).signed(signer).into()
+ pub fn node(message: NodeAnnouncement, secret_key: &crypto::SigningKey) -> Self {
+ AnnouncementMessage::from(message).signed(secret_key).into()
}
- pub fn inventory<G: crypto::signature::Signer<crypto::Signature>>(
- message: InventoryAnnouncement,
- signer: &Device<G>,
- ) -> Self {
- AnnouncementMessage::from(message).signed(signer).into()
+ pub fn inventory(message: InventoryAnnouncement, secret_key: &crypto::SigningKey) -> Self {
+ AnnouncementMessage::from(message).signed(secret_key).into()
}
pub fn subscribe(filter: Filter, since: Timestamp, until: Timestamp) -> Self {
@@ -684,9 +682,9 @@ impl qcheck::Arbitrary for ZeroBytes {
#[allow(clippy::unwrap_used)]
mod tests {
- use fastrand;
use localtime::LocalTime;
use qcheck_macros::quickcheck;
+ use radicle::crypto::SigningKey;
use radicle::test::arbitrary;
use crate::wire::Decode as _;
@@ -696,7 +694,7 @@ mod tests {
#[test]
fn test_ref_remote_limit() {
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
- let signer = Device::mock();
+ let signer = SigningKey::mock(18);
let at = git::Oid::ZERO_SHA1;
assert_eq!(refs.capacity(), REF_REMOTE_LIMIT);
@@ -714,7 +712,7 @@ mod tests {
refs,
timestamp: LocalTime::now().into(),
})
- .signed(&Device::mock())
+ .signed(&crypto::SigningKey::mock(93))
.into();
let mut buf = Vec::new();
@@ -734,7 +732,7 @@ mod tests {
.expect("size within bounds limit"),
timestamp: LocalTime::now().into(),
},
- &Device::mock(),
+ &crypto::SigningKey::mock(218),
);
let mut buf: Vec<u8> = Vec::new();
msg.encode(&mut buf);
@@ -753,12 +751,12 @@ mod tests {
#[quickcheck]
fn prop_refs_announcement_signing(rid: RepoId) {
- let signer = Device::mock_rng(&mut fastrand::Rng::new());
+ let secret_key = crypto::SigningKey::mock(242);
let timestamp = Timestamp::EPOCH;
let at = git::Oid::ZERO_SHA1;
let refs = BoundedVec::collect_from(
&mut [RefsAt {
- remote: *signer.public_key(),
+ remote: *secret_key.public_key(),
at,
}]
.into_iter(),
@@ -768,7 +766,7 @@ mod tests {
refs,
timestamp,
});
- let ann = message.signed(&signer);
+ let ann = message.signed(&secret_key);
assert!(ann.verify());
}
diff --git a/crates/radicle-protocol/src/wire.rs b/crates/radicle-protocol/src/wire.rs
index 578b4deae..a45e5acf0 100644
--- a/crates/radicle-protocol/src/wire.rs
+++ b/crates/radicle-protocol/src/wire.rs
@@ -18,7 +18,7 @@ use cypheraddr::i2p;
#[cfg(feature = "tor")]
use cypheraddr::tor;
-use radicle::crypto::{PublicKey, Signature};
+use radicle::crypto::{PublicKey, PublicKeyBytes, Signature};
use radicle::git;
use radicle::git::fmt;
use radicle::identity::RepoId;
@@ -181,7 +181,7 @@ impl Encode for u64 {
impl Encode for PublicKey {
fn encode(&self, buf: &mut impl BufMut) {
- self.to_byte_array().encode(buf)
+ std::borrow::Borrow::<PublicKeyBytes>::borrow(self).encode(buf)
}
}
@@ -310,7 +310,8 @@ impl Encode for git::fmt::RefString {
impl Encode for Signature {
fn encode(&self, buf: &mut impl BufMut) {
- self.deref().encode(buf)
+ let bytes: [u8; 64] = self.to_bytes();
+ bytes.encode(buf)
}
}
@@ -326,9 +327,7 @@ impl Encode for git::Oid {
impl Decode for PublicKey {
fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
- let buf: [u8; 32] = Decode::decode(buf)?;
-
- Ok(PublicKey::from(buf))
+ Ok(PublicKey::from(<[u8; 32]>::decode(buf)?))
}
}
diff --git a/crates/radicle-protocol/src/wire/message.rs b/crates/radicle-protocol/src/wire/message.rs
index f574a17f9..9e615db99 100644
--- a/crates/radicle-protocol/src/wire/message.rs
+++ b/crates/radicle-protocol/src/wire/message.rs
@@ -409,7 +409,6 @@ impl wire::Decode for ZeroBytes {
mod tests {
use qcheck_macros::quickcheck;
use radicle::node::UserAgent;
- use radicle::node::device::Device;
use radicle::storage::refs::RefsAt;
use radicle::test::arbitrary;
@@ -424,7 +423,7 @@ mod tests {
#[test]
fn test_refs_ann_max_size() {
- let signer = Device::mock();
+ let signer = radicle::crypto::SigningKey::mock(235);
let refs: [RefsAt; REF_REMOTE_LIMIT] = arbitrary::r#gen(1);
let ann = AnnouncementMessage::Refs(RefsAnnouncement {
rid: arbitrary::r#gen(1),
@@ -440,7 +439,7 @@ mod tests {
#[test]
fn test_inv_ann_max_size() {
- let signer = Device::mock();
+ let signer = radicle::crypto::SigningKey::mock(147);
let inv: [RepoId; INVENTORY_LIMIT] = arbitrary::r#gen(1);
let ann = AnnouncementMessage::Inventory(InventoryAnnouncement {
inventory: BoundedVec::collect_from(&mut inv.into_iter()),
@@ -455,7 +454,7 @@ mod tests {
#[test]
fn test_node_ann_max_size() {
- let signer = Device::mock();
+ let signer = radicle::crypto::SigningKey::mock(247);
let addrs: [Address; ADDRESS_LIMIT] = arbitrary::r#gen(1);
let alias = ['@'; radicle::node::MAX_ALIAS_LENGTH];
let ann = AnnouncementMessage::Node(NodeAnnouncement {
diff --git a/crates/radicle-remote-helper/src/main.rs b/crates/radicle-remote-helper/src/main.rs
index 5fc451959..68b2c9a01 100644
--- a/crates/radicle-remote-helper/src/main.rs
+++ b/crates/radicle-remote-helper/src/main.rs
@@ -28,9 +28,10 @@ use std::process;
use std::str::FromStr;
use std::{env, fmt};
-use radicle::cob::store::access::{ReadOnly, WriteAs};
use thiserror::Error;
+use radicle::cob::store::access::{ReadOnly, WriteAs};
+use radicle::crypto;
use radicle::prelude::NodeId;
use radicle::storage::git::transport::local::{Url, UrlError};
use radicle::storage::{ReadRepository, WriteStorage};
@@ -482,7 +483,7 @@ pub(crate) fn patches<'a, Repo: ReadRepository + cob::Store<Namespace = NodeId>>
}
/// Get the mutable patch store.
-pub(crate) fn patches_mut<'a, 'b, Signer>(
+pub(crate) fn patches_mut<'a, 'b, Signer: crypto::Signer>(
profile: &Profile,
repo: &'a storage::git::Repository,
signer: &'b Signer,
diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs
index 2efac2d57..7bc6380da 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -16,6 +16,7 @@ use radicle::cob::object::ParseObjectId;
use radicle::cob::patch;
use radicle::cob::patch::cache::Patches as _;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::explorer::ExplorerResource;
use radicle::identity::Did;
use radicle::node;
@@ -287,7 +288,7 @@ pub(super) fn run(
// won't match the remote we're pushing to.
// 3. The URL namespace is not set.
let nid = url.namespace.ok_or(Error::NoKey).and_then(|ns| {
- (profile.public_key == ns)
+ (profile.id() == &ns)
.then_some(ns)
.ok_or(Error::KeyMismatch(ns.into()))
})?;
@@ -568,7 +569,7 @@ impl<'a, G> Drop for TempPatchRef<'a, G> {
}
/// Open a new patch.
-fn patch_open<S, Signer>(
+fn patch_open(
head: &git::Oid,
upstream: &Option<git::fmt::RefString>,
nid: &NodeId,
@@ -577,20 +578,13 @@ fn patch_open<S, Signer>(
mut patches: patch::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::StoreWriter,
>,
profile: &Profile,
opts: Options,
- git: &S,
-) -> Result<Option<ExplorerResource>, Error>
-where
- S: GitService,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+ git: &impl GitService,
+) -> Result<Option<ExplorerResource>, Error> {
let temp = TempPatchRef::new(stored, head, nid, git);
temp.push(head, opts.verbosity)?;
let base = patch_base(head, &opts, stored)?;
@@ -692,7 +686,7 @@ where
/// Update an existing patch.
#[allow(clippy::too_many_arguments)]
-fn patch_update<S, Signer>(
+fn patch_update<Signer>(
head: &git::Oid,
dst: &git::fmt::Qualified,
force: bool,
@@ -709,14 +703,10 @@ fn patch_update<S, Signer>(
signer: &Signer,
opts: Options,
expected_refs: &[String],
- git: &S,
+ git: &impl GitService,
) -> Result<Option<ExplorerResource>, Error>
where
- S: GitService,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let Ok(Some(patch)) = patches.get(&patch_id) else {
return Err(Error::NotFound(patch_id));
@@ -786,7 +776,7 @@ where
Ok(Some(ExplorerResource::Patch { id: patch_id }))
}
-fn push<S, Signer>(
+fn push<Signer>(
src: &git::Oid,
dst: &git::fmt::Qualified,
force: bool,
@@ -802,14 +792,10 @@ fn push<S, Signer>(
signer: &Signer,
verbosity: Verbosity,
expected_refs: &[String],
- git: &S,
+ git: &impl GitService,
) -> Result<Option<ExplorerResource>, Error>
where
- S: GitService,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let head = *src;
let namespaced_dst = dst.with_namespace(nid.into());
@@ -859,7 +845,7 @@ where
}
/// Revert all patches that are no longer included in the base branch.
-fn patch_revert_all<Signer>(
+fn patch_revert_all(
old: git::Oid,
new: git::Oid,
pushed_dst: &git::fmt::Qualified,
@@ -867,15 +853,11 @@ fn patch_revert_all<Signer>(
patches: &mut patch::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::StoreWriter,
>,
identity: &radicle::identity::Identity,
-) -> Result<(), Error>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
-{
+) -> Result<(), Error> {
// Find all commits reachable from the old OID but not from the new OID.
let mut revwalk = stored.revwalk()?;
revwalk.push(old.into())?;
@@ -961,10 +943,7 @@ fn patch_merge_all<Signer>(
identity: &radicle::identity::Identity,
) -> Result<(), Error>
where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let mut revwalk = working.revwalk()?;
revwalk.push_range(&format!("{old}..{new}"))?;
@@ -1011,19 +990,22 @@ where
Ok(())
}
-fn patch_merge<Signer, C>(
- mut patch: patch::PatchMut<'_, '_, '_, storage::git::Repository, Signer, C>,
+fn patch_merge<Signer>(
+ mut patch: patch::PatchMut<
+ '_,
+ '_,
+ '_,
+ storage::git::Repository,
+ Signer,
+ impl cob::cache::Update<patch::Patch>,
+ >,
revision: patch::RevisionId,
commit: git::Oid,
working: &git::raw::Repository,
signer: &Signer,
) -> Result<(), Error>
where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- C: cob::cache::Update<patch::Patch>,
+ Signer: crypto::Signer,
{
let (latest, _) = patch.latest();
let merged = patch.merge(revision, commit)?;
diff --git a/crates/radicle/Cargo.toml b/crates/radicle/Cargo.toml
index 529c0996e..2c1193d74 100644
--- a/crates/radicle/Cargo.toml
+++ b/crates/radicle/Cargo.toml
@@ -39,11 +39,12 @@ fastrand = { workspace = true, features = ["std"] }
git2 = { workspace = true, features = ["vendored-libgit2"] }
indexmap = { workspace = true, features = ["serde"] }
log = { workspace = true, features = ["std"] }
+multibase = { workspace = true, features = ["std"] }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle-cob = { workspace = true, features = ["git2"] }
radicle-core = { workspace = true, features = ["git2", "serde", "sqlite"] }
-radicle-crypto = { workspace = true, features = ["git-ref-format-core", "ssh", "sqlite", "cyphernet"] }
+radicle-crypto = { workspace = true, features = ["git-ref-format-core", "serde", "ssh", "sqlite", "cyphernet"] }
radicle-git-ref-format = { workspace = true, features = ["macro", "serde"] }
radicle-git-metadata = { workspace = true }
radicle-localtime = { workspace = true, features = ["serde"] }
diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs
index 6429fea5a..1384315f6 100644
--- a/crates/radicle/src/cob/identity.rs
+++ b/crates/radicle/src/cob/identity.rs
@@ -65,6 +65,7 @@ pub enum Action {
/// Parent revision that this revision replaces.
parent: Option<RevisionId>,
/// Signature over the revision blob.
+ #[serde(with = "signature")]
signature: Signature,
},
RevisionEdit {
@@ -80,6 +81,7 @@ pub enum Action {
RevisionAccept {
revision: RevisionId,
/// Signature over the blob.
+ #[serde(with = "signature")]
signature: Signature,
},
#[serde(rename = "revision.reject")]
@@ -214,10 +216,7 @@ impl Identity {
) -> Result<IdentityMut<'a, 'b, Repo, Signer>, cob::store::Error>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let mut store = cob::store::Store::open(store, WriteAs::new(signer))?;
@@ -262,7 +261,7 @@ impl Identity {
) -> Result<IdentityMut<'a, 'b, Repo, Signer>, store::Error>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
let obj = Self::get(id, repo)?;
let store = cob::store::Store::open(repo, WriteAs::new(signer))?;
@@ -287,7 +286,7 @@ impl Identity {
) -> Result<IdentityMut<'a, 'b, Repo, Signer>, RepositoryError>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
let oid = repo.identity_root()?;
let oid = ObjectId::from(oid);
@@ -894,7 +893,7 @@ impl<R: ReadRepository> cob::Evaluate<R> for Identity {
pub enum Verdict {
/// An accepting verdict must supply the [`Signature`] over the
/// new proposed [`Doc`].
- Accept(Signature),
+ Accept(#[serde(with = "signature")] Signature),
/// Rejecting the proposed [`Doc`].
Reject,
}
@@ -1067,10 +1066,7 @@ impl Revision {
})
}
- pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
- &self,
- signer: &G,
- ) -> Result<Signature, DocError> {
+ pub fn sign(&self, signer: &impl crypto::Signer) -> Result<Signature, DocError> {
self.doc.signature_of(signer)
}
}
@@ -1141,14 +1137,14 @@ impl<R: ReadRepository> store::Transaction<Identity, R> {
}
}
-impl<R: WriteRepository> store::Transaction<Identity, R> {
- pub fn new_revision<G: crypto::signature::Signer<crypto::Signature>>(
+impl<Repo: WriteRepository> store::Transaction<Identity, Repo> {
+ pub fn new_revision(
title: cob::Title,
description: impl ToString,
doc: &Doc,
parent: Option<RevisionId>,
- repo: &R,
- signer: &G,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
) -> Result<Self, store::Error> {
let mut tx = Transaction::default();
@@ -1175,14 +1171,14 @@ impl<R: WriteRepository> store::Transaction<Identity, R> {
}
}
-pub struct IdentityMut<'a, 'b, Repo, Signer> {
+pub struct IdentityMut<'a, 'b, Repo, Signer: crypto::Signer> {
pub id: ObjectId,
identity: Identity,
store: store::Store<'a, Identity, Repo, WriteAs<'b, Signer>>,
}
-impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
+impl<Repo, Signer: crypto::Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IdentityMut")
.field("id", &self.id)
@@ -1191,13 +1187,9 @@ impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
}
}
-impl<Repo, Signer> IdentityMut<'_, '_, Repo, Signer>
+impl<Repo, Signer: crypto::Signer> IdentityMut<'_, '_, Repo, Signer>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
{
/// Reload the identity data from storage.
#[cfg(test)]
@@ -1240,7 +1232,7 @@ where
doc: &Doc,
) -> Result<RevisionId, Error> {
#[allow(deprecated)]
- let did: Did = self.store.signer().verifying_key().into();
+ let did: Did = self.store.signer().public_key().into();
if let Some(revision_id) = self.has_accepted_active_sibling(&self.current, &did) {
return Err(Error::Apply(ApplyError::SiblingAccepted {
revision: revision_id,
@@ -1275,7 +1267,7 @@ where
if let Some(parent_id) = revision.parent {
#[allow(deprecated)]
- let did: Did = self.store.signer().verifying_key().into();
+ let did: Did = self.store.signer().public_key().into();
if let Some(revision_id) = self.has_accepted_active_sibling(&parent_id, &did) {
return Err(Error::Apply(ApplyError::SiblingAccepted {
revision: revision_id,
@@ -1312,7 +1304,7 @@ where
}
}
-impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
+impl<Repo, Signer: crypto::Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
type Target = Identity;
fn deref(&self) -> &Self::Target {
@@ -1320,6 +1312,29 @@ impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
}
}
+mod signature {
+ pub fn serialize<S>(signature: &crypto::Signature, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&multibase::encode(
+ multibase::Base::Base58Btc,
+ signature.to_bytes(),
+ ))
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<crypto::Signature, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ use serde::Deserialize;
+
+ let s = String::deserialize(deserializer)?;
+ let (_, bytes) = multibase::decode(&s).map_err(serde::de::Error::custom)?;
+ crypto::Signature::from_slice(bytes.as_slice()).map_err(serde::de::Error::custom)
+ }
+}
+
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
@@ -1328,11 +1343,10 @@ mod test {
use qcheck_macros::quickcheck;
use crate::cob::{self, Title};
- use crate::crypto::PublicKey;
+ use crate::crypto::{PublicKey, Signer as _, SigningKey};
use crate::identity::Visibility;
use crate::identity::did::Did;
use crate::identity::doc::PayloadId;
- use crate::node::device::Device;
use crate::rad;
use crate::storage::ReadStorage as _;
use crate::storage::git::Storage;
@@ -1356,7 +1370,7 @@ mod test {
#[test]
fn test_identity_updates() {
let NodeWithRepo { node, repo } = NodeWithRepo::default();
- let bob = Device::mock();
+ let bob = SigningKey::mock(103);
let signer = &node.signer;
let mut identity = Identity::load_mut(&*repo, signer).unwrap();
let mut doc = identity.doc().clone().edit();
@@ -1419,8 +1433,8 @@ mod test {
#[test]
fn test_identity_update_rejected() {
let NodeWithRepo { node, repo } = NodeWithRepo::default();
- let bob = Device::mock();
- let eve = Device::mock();
+ let bob = SigningKey::mock(200);
+ let eve = SigningKey::mock(201);
let signer = &node.signer;
let mut identity = Identity::load_mut(&*repo, signer).unwrap();
@@ -2531,9 +2545,9 @@ mod test {
let tempdir = tempfile::tempdir().unwrap();
let mut rng = fastrand::Rng::new();
- let alice = Device::mock_rng(&mut rng);
- let bob = Device::mock_rng(&mut rng);
- let eve = Device::mock_rng(&mut rng);
+ let alice = SigningKey::mock(rng.usize(..));
+ let bob = SigningKey::mock(rng.usize(..));
+ let eve = SigningKey::mock(rng.usize(..));
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
let (id, _, _, _) =
@@ -2717,7 +2731,6 @@ mod test {
/// enables a queued child to be automatically adopted.
#[test]
fn evaluates_queued_children_with_new_delegate() {
- use crate::crypto::test::signer::MockSigner;
use crate::test::setup::{Node, NodeRepo};
use tempfile::tempdir;
@@ -2727,7 +2740,7 @@ mod test {
let eve = &network.eve;
// Create Dave as a 4th participant.
- let mut dave_node = Node::new(tempdir().unwrap(), MockSigner::from_seed([!3; 32]), "dave");
+ let mut dave_node = Node::new(tempdir().unwrap(), SigningKey::mock(3), "dave");
dave_node.clone(network.rid, alice);
let dave_repo = NodeRepo {
repo: dave_node.storage.repository(network.rid).unwrap(),
@@ -2829,7 +2842,6 @@ mod test {
/// by its parent, not by the currently accepted identity document.
#[test]
fn authorization_based_on_parent_not_current() {
- use crate::crypto::test::signer::MockSigner;
use crate::test::setup::{Node, NodeRepo};
use tempfile::tempdir;
@@ -2839,7 +2851,7 @@ mod test {
let eve = &network.eve;
// Create Dave as a 4th participant.
- let mut dave_node = Node::new(tempdir().unwrap(), MockSigner::from_seed([!3; 32]), "dave");
+ let mut dave_node = Node::new(tempdir().unwrap(), SigningKey::mock(3), "dave");
dave_node.clone(network.rid, alice);
let dave_repo = NodeRepo {
repo: dave_node.storage.repository(network.rid).unwrap(),
diff --git a/crates/radicle/src/cob/identity/test/property.rs b/crates/radicle/src/cob/identity/test/property.rs
index 3e79d0965..1d1b4cc31 100644
--- a/crates/radicle/src/cob/identity/test/property.rs
+++ b/crates/radicle/src/cob/identity/test/property.rs
@@ -4,6 +4,7 @@ use crate::assert_matches;
use crate::cob;
use crate::cob::identity::{Did, Identity, RedactedBy, RejectedBy, RevisionId, State};
use crate::cob::store::Transaction;
+use crate::crypto::Signer as _;
use crate::identity::doc::PayloadId;
use crate::test::arbitrary::BoundedVec;
use crate::test::setup::Network;
@@ -192,7 +193,7 @@ impl Harness {
'_,
'_,
crate::storage::git::Repository,
- crate::node::device::Device<crypto::test::signer::MockSigner>,
+ crypto::SigningKey,
>,
) -> Result<T, cob::identity::Error>,
{
@@ -465,12 +466,8 @@ impl Harness {
fn identity_for(
&self,
actor: &Actor,
- ) -> cob::identity::IdentityMut<
- '_,
- '_,
- crate::storage::git::Repository,
- crate::node::device::Device<crypto::test::signer::MockSigner>,
- > {
+ ) -> cob::identity::IdentityMut<'_, '_, crate::storage::git::Repository, crypto::SigningKey>
+ {
let (_, identity, _) = self.signer_identity_doc_for(actor);
identity
@@ -480,13 +477,8 @@ impl Harness {
&self,
actor: &Actor,
) -> (
- &crate::node::device::Device<crypto::test::signer::MockSigner>,
- cob::identity::IdentityMut<
- '_,
- '_,
- crate::storage::git::Repository,
- crate::node::device::Device<crypto::test::signer::MockSigner>,
- >,
+ &crypto::SigningKey,
+ cob::identity::IdentityMut<'_, '_, crate::storage::git::Repository, crypto::SigningKey>,
crate::prelude::RawDoc,
) {
let (repo, signer) = match actor {
diff --git a/crates/radicle/src/cob/issue.rs b/crates/radicle/src/cob/issue.rs
index d7214e52d..6f74cf76e 100644
--- a/crates/radicle/src/cob/issue.rs
+++ b/crates/radicle/src/cob/issue.rs
@@ -375,7 +375,7 @@ impl Issue {
Action::CommentEdit { id, .. } | Action::CommentRedact { id, .. } => {
if let Some(comment) = self.thread.comments.get(id) {
if let Some(comment) = comment {
- Authorization::from(*actor == comment.author())
+ Authorization::from(actor == comment.author())
} else {
Authorization::Unknown
}
@@ -472,8 +472,8 @@ impl Issue {
}
}
-impl<'a, 'b, 'g, Repo, Signer, Cache> From<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>>
- for (IssueId, Issue)
+impl<'a, 'b, 'g, Repo, Signer: crypto::Signer, Cache>
+ From<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>> for (IssueId, Issue)
{
fn from(value: IssueMut<'a, 'b, 'g, Repo, Signer, Cache>) -> Self {
(value.id, value.issue)
@@ -581,14 +581,16 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
}
}
-pub struct IssueMut<'a, 'b, 'g, Repo, Signer, Cache> {
+pub struct IssueMut<'a, 'b, 'g, Repo, Signer: crypto::Signer, Cache> {
id: ObjectId,
issue: Issue,
store: &'g mut Issues<'a, Repo, WriteAs<'b, Signer>>,
cache: &'g mut Cache,
}
-impl<Repo, Signer, Cache> std::fmt::Debug for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
+impl<Repo, Signer: crypto::Signer, Cache> std::fmt::Debug
+ for IssueMut<'_, '_, '_, Repo, Signer, Cache>
+{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("IssueMut")
.field("id", &self.id)
@@ -597,13 +599,9 @@ impl<Repo, Signer, Cache> std::fmt::Debug for IssueMut<'_, '_, '_, Repo, Signer,
}
}
-impl<Repo, Signer, Cache> IssueMut<'_, '_, '_, Repo, Signer, Cache>
+impl<Repo, Signer: crypto::Signer, Cache> IssueMut<'_, '_, '_, Repo, Signer, Cache>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
Cache: cob::cache::Update<Issue>,
{
/// Reload the issue data from storage.
@@ -694,10 +692,6 @@ where
pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error>
where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
F: FnOnce(&mut Transaction<Issue, Repo>) -> Result<(), store::Error>,
{
let mut tx = Transaction::default();
@@ -716,7 +710,7 @@ where
}
}
-impl<Repo, Signer, Cache> Deref for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
+impl<Repo, Signer: crypto::Signer, Cache> Deref for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
type Target = Issue;
fn deref(&self) -> &Self::Target {
@@ -777,10 +771,7 @@ where
impl<'a, 'b, Repo, Signer> Issues<'a, Repo, WriteAs<'b, Signer>>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Get an issue mutably.
pub fn get_mut<'g, Cache>(
@@ -947,9 +938,9 @@ mod test {
use super::*;
use crate::cob::{ActorId, Reaction, store::CobWithType};
+ use crate::crypto::{Signer as _, SigningKey};
use crate::git::Oid;
use crate::issue::cache::Issues as _;
- use crate::node::device::Device;
use crate::test::arbitrary;
use crate::{assert_matches, test};
@@ -1169,8 +1160,8 @@ mod test {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
- let assignee = Did::from(arbitrary::r#gen::<ActorId>(1));
- let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1));
+ let [assignee, assignee_two] = arbitrary::vec_distinct::<Did>(2..=2).try_into().unwrap();
+
let mut issue = issues
.create(
cob::Title::new("My first issue").unwrap(),
@@ -1264,7 +1255,7 @@ mod test {
let reactions = issue.comment(&comment).unwrap().reactions();
let authors = reactions.get(&reaction).unwrap();
- assert_eq!(authors.first().unwrap(), &node.signer.public_key());
+ assert_eq!(*authors.first().unwrap(), node.signer.public_key());
// TODO: Test multiple reactions from same author and different authors
}
@@ -1349,7 +1340,7 @@ mod test {
#[test]
fn test_issue_comment() {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
- let author = *node.signer.public_key();
+ let author = node.signer.public_key();
let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
let mut issue = issues
.create(
@@ -1667,7 +1658,7 @@ mod test {
use nonempty::NonEmpty;
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
- let eve = Device::mock();
+ let eve = SigningKey::mock(126);
let identity = repo.identity().unwrap().head();
let missing = arbitrary::oid();
let type_name = Issue::type_name().clone();
diff --git a/crates/radicle/src/cob/issue/cache.rs b/crates/radicle/src/cob/issue/cache.rs
index 94617ad9f..d4af802ab 100644
--- a/crates/radicle/src/cob/issue/cache.rs
+++ b/crates/radicle/src/cob/issue/cache.rs
@@ -97,13 +97,7 @@ where
}
}
-impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+impl<'a, 'b, Repo, Signer: crypto::Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C> {
/// Create a new [`Issue`] using the [`super::Issues`] as the
/// main storage, and writing the update to the `cache`.
pub fn create<'g>(
@@ -210,7 +204,7 @@ where
impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Get a `Cache` that does no write-through modifications and
/// uses the [`super::Issues`] store for all reads and writes.
@@ -253,7 +247,7 @@ impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> {
}
}
-impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
+impl<'a, 'b, Repo, Signer: crypto::Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
diff --git a/crates/radicle/src/cob/op.rs b/crates/radicle/src/cob/op.rs
index 4c49b807a..0bf976c58 100644
--- a/crates/radicle/src/cob/op.rs
+++ b/crates/radicle/src/cob/op.rs
@@ -124,7 +124,8 @@ impl<A> Op<A> {
S: cob::change::Storage<
ObjectId = git::Oid,
Parent = git::Oid,
- Signatures = crypto::ssh::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
store.manifest_of(id).map_err(|err| ManifestError {
@@ -139,7 +140,8 @@ impl<A> Op<A> {
S: cob::change::Storage<
ObjectId = git::Oid,
Parent = git::Oid,
- Signatures = crypto::ssh::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
for<'de> A: serde::Deserialize<'de>,
{
diff --git a/crates/radicle/src/cob/patch.rs b/crates/radicle/src/cob/patch.rs
index d378e764d..451bd3349 100644
--- a/crates/radicle/src/cob/patch.rs
+++ b/crates/radicle/src/cob/patch.rs
@@ -416,17 +416,12 @@ impl<R: WriteRepository> Merged<'_, R> {
///
/// This removes Git refs relating to the patch, both in the working copy,
/// and the stored copy; and updates `rad/sigrefs`.
- pub fn cleanup<Signer>(
+ pub fn cleanup(
self,
working: &git::raw::Repository,
- signer: &Signer,
- ) -> Result<(), storage::RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
- let nid = &signer.verifying_key();
+ signer: &impl crypto::Signer,
+ ) -> Result<(), storage::RepositoryError> {
+ let nid = signer.public_key();
let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into());
let working_ref = git::refs::workdir::patch_upstream(&self.patch);
@@ -899,7 +894,7 @@ impl Patch {
if let Some((_, review)) = lookup::review(self, review)?
&& let Some(comment) = review.comments.comment(comment)
{
- return Ok(Authorization::from(*actor == comment.author()));
+ return Ok(Authorization::from(actor == comment.author()));
}
// Redacted.
Authorization::Unknown
@@ -913,7 +908,7 @@ impl Patch {
&& let Some(comment) = review.comments.comment(comment)
{
return Ok(Authorization::from(
- actor == &comment.author()
+ actor == comment.author()
|| actor == review.author.public_key()
|| actor == revision.author.public_key(),
));
@@ -946,7 +941,7 @@ impl Patch {
if let Some(revision) = lookup::revision(self, revision)?
&& let Some(comment) = revision.discussion.comment(comment)
{
- return Ok(Authorization::from(actor == &comment.author()));
+ return Ok(Authorization::from(actor == comment.author()));
}
// Redacted.
Authorization::Unknown
@@ -2207,7 +2202,7 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
}
}
-pub struct PatchMut<'a, 'b, 'g, Repo, Signer, Cache> {
+pub struct PatchMut<'a, 'b, 'g, Repo, Signer: crypto::Signer, Cache> {
pub id: ObjectId,
patch: Patch,
@@ -2215,13 +2210,9 @@ pub struct PatchMut<'a, 'b, 'g, Repo, Signer, Cache> {
cache: &'g mut Cache,
}
-impl<'a, 'b, 'g, Repo, Signer, Update> PatchMut<'a, 'b, 'g, Repo, Signer, Update>
+impl<'a, 'b, 'g, Repo, Signer: crypto::Signer, Update> PatchMut<'a, 'b, 'g, Repo, Signer, Update>
where
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
Update: cob::cache::Update<Patch>,
{
pub fn new(
@@ -2575,7 +2566,7 @@ where
}
}
-impl<Repo, Signer, Cache> Deref for PatchMut<'_, '_, '_, Repo, Signer, Cache> {
+impl<Repo, Signer: crypto::Signer, Cache> Deref for PatchMut<'_, '_, '_, Repo, Signer, Cache> {
type Target = Patch;
fn deref(&self) -> &Self::Target {
@@ -2725,8 +2716,7 @@ where
impl<'a, 'b, Repo, Signer> Patches<'a, Repo, WriteAs<'b, Signer>>
where
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Open a new patch.
pub fn create<'g, Cache>(
@@ -2741,9 +2731,7 @@ where
) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
where
Cache: cob::cache::Update<Patch>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
self._create(
title,
@@ -2770,8 +2758,6 @@ where
) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
where
Cache: cob::cache::Update<Patch>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
{
self._create(
title,
@@ -2818,10 +2804,7 @@ where
) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
where
Cache: cob::cache::Update<Patch>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let (id, patch) = Transaction::initial("Create patch", &mut self.raw, |tx, _| {
tx.revision(description, base, oid)?;
@@ -3039,8 +3022,8 @@ mod test {
use super::*;
use crate::cob::common::CodeRange;
- use crate::cob::test::Actor;
- use crate::crypto::test::signer::MockSigner;
+ use crate::cob::test::SignerOpExt as _;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::git::BranchName;
use crate::identity;
use crate::identity::doc::RawDoc;
@@ -3133,7 +3116,7 @@ mod test {
#[test]
fn test_merge_target_resolution() {
- let alice = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(23);
let project = Project::new(
ProjectName::from_str("test_merge_target_resolution").unwrap(),
String::from(""),
@@ -3190,7 +3173,7 @@ mod test {
fn test_patch_merge_authorization_ref_formats() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let alice = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(14);
let mut raw_doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3273,8 +3256,8 @@ mod test {
fn test_patch_merge_custom_destination_authorized() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let alice = Actor::<MockSigner>::default();
- let bob = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(14);
+ let bob = SigningKey::mock(23);
let mut raw_doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3332,8 +3315,8 @@ mod test {
fn test_patch_merge_custom_destination_unauthorized() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let alice = Actor::<MockSigner>::default();
- let bob = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(14);
+ let bob = SigningKey::mock(23);
let mut raw_doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3627,7 +3610,7 @@ mod test {
fn test_revision_review_merge_redacted() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(14);
let rid = r#gen::<RepoId>(1);
let doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3687,8 +3670,8 @@ mod test {
let oid = arbitrary::oid();
let repo = r#gen::<MockRepository>(1);
let time = env::local_time();
- let alice = MockSigner::default();
- let bob = MockSigner::default();
+ let alice = SigningKey::mock(38);
+ let bob = SigningKey::mock(39);
let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history(
&[
Action::Revision {
@@ -3746,7 +3729,7 @@ mod test {
fn test_revision_reaction() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(35);
let repo = r#gen::<MockRepository>(1);
let reaction = Reaction::new('👍').expect("failed to create a reaction");
diff --git a/crates/radicle/src/cob/patch/cache.rs b/crates/radicle/src/cob/patch/cache.rs
index 71f390074..3b0541567 100644
--- a/crates/radicle/src/cob/patch/cache.rs
+++ b/crates/radicle/src/cob/patch/cache.rs
@@ -107,13 +107,7 @@ where
}
}
-impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+impl<'a, 'b, Repo, Signer: crypto::Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C> {
/// Create a new [`Patch`] using the [`super::Patches`] as the
/// main storage, and writing the update to the `cache`.
pub fn create<'g>(
@@ -258,7 +252,7 @@ impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> {
}
}
-impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
+impl<'a, 'b, Repo, Signer: crypto::Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
@@ -280,7 +274,7 @@ where
}
}
-impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
+impl<'a, 'b, Repo, Signer: crypto::Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
diff --git a/crates/radicle/src/cob/store.rs b/crates/radicle/src/cob/store.rs
index 41b5941f2..3a0fd102c 100644
--- a/crates/radicle/src/cob/store.rs
+++ b/crates/radicle/src/cob/store.rs
@@ -233,7 +233,7 @@ where
}
}
-impl<'a, 'b, T, Repo, Signer> Store<'a, T, Repo, WriteAs<'b, Signer>>
+impl<'a, 'b, T, Repo, Signer: crypto::Signer> Store<'a, T, Repo, WriteAs<'b, Signer>>
where
T: Cob + cob::Evaluate<Repo>,
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
@@ -262,10 +262,7 @@ where
T: Cob + cob::Evaluate<Repo>,
T::Action: Serialize,
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Update an object.
pub fn update(
@@ -288,13 +285,13 @@ where
})
})
.collect::<Result<_, _>>()?;
- let namespace = self.access.signer.verifying_key();
+ let namespace = self.access.signer.public_key();
let updated = cob::update(
self.repo,
self.access.signer,
self.identity,
related,
- &namespace,
+ namespace,
Update {
object_id,
type_name: type_name.clone(),
@@ -329,13 +326,13 @@ where
})
})
.collect::<Result<_, _>>()?;
- let namespace = self.access.signer.verifying_key();
- let cob = cob::create::<T, _, _>(
+ let namespace = self.access.signer.public_key();
+ let cob = cob::create::<T, _>(
self.repo,
self.access.signer,
self.identity,
parents,
- &namespace,
+ namespace,
Create {
type_name: self.scope.type_name.clone(),
version: Version::default(),
@@ -357,11 +354,11 @@ where
/// Remove an object.
pub fn remove(&mut self, id: &ObjectId) -> Result<(), Error> {
- let namespace = self.access.signer.verifying_key();
- let name = git::refs::storage::cob(&namespace, self.scope.type_name, id);
- match self.repo.reference_oid(&namespace, &name.strip_namespace()) {
+ let namespace = self.access.signer.public_key();
+ let name = git::refs::storage::cob(namespace, self.scope.type_name, id);
+ match self.repo.reference_oid(namespace, &name.strip_namespace()) {
Ok(_) => {
- cob::remove(self.repo, &namespace, self.scope.type_name, id)?;
+ cob::remove(self.repo, namespace, self.scope.type_name, id)?;
self.repo
.sign_refs(self.access.signer)
.map_err(|e| Error::SignRefs(Box::new(e)))?;
@@ -469,18 +466,14 @@ where
T: Cob + CobWithType + cob::Evaluate<Repo>,
{
/// Create a new transaction to be used as the initial set of operations for a COB.
- pub fn initial<'a, 'b, Signer, Tx, F>(
+ pub fn initial<'a, 'b, Tx, F>(
message: &str,
- store: &mut Store<'a, T, Repo, WriteAs<'b, Signer>>,
+ store: &mut Store<'a, T, Repo, WriteAs<'b, impl crypto::Signer>>,
operations: F,
) -> Result<(ObjectId, T), Error>
where
T::Action: Serialize + Clone,
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
Tx: From<Self>,
Self: From<Tx>,
F: FnOnce(&mut Tx, &Repo) -> Result<(), Error>,
@@ -538,19 +531,15 @@ where
/// Commit transaction.
///
/// Returns an operation that can be applied onto an in-memory state.
- pub fn commit<'a, Signer>(
+ pub fn commit<'a>(
self,
msg: &str,
id: ObjectId,
- store: &mut Store<T, Repo, WriteAs<'a, Signer>>,
+ store: &mut Store<T, Repo, WriteAs<'a, impl crypto::Signer>>,
) -> Result<(T, EntryId), Error>
where
T::Action: Serialize + Clone,
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
{
let actions = NonEmpty::from_vec(self.actions)
.expect("Transaction::commit: transaction must not be empty");
diff --git a/crates/radicle/src/cob/store/access.rs b/crates/radicle/src/cob/store/access.rs
index afccd4eed..c589f9efa 100644
--- a/crates/radicle/src/cob/store/access.rs
+++ b/crates/radicle/src/cob/store/access.rs
@@ -7,11 +7,11 @@ pub use seal::Access;
pub struct ReadOnly;
/// [`WriteAs`] is used for write [`Access`].
-pub struct WriteAs<'a, Signer> {
+pub struct WriteAs<'a, Signer: crypto::Signer> {
pub(super) signer: &'a Signer,
}
-impl<'a, Signer> WriteAs<'a, Signer> {
+impl<'a, Signer: crypto::Signer> WriteAs<'a, Signer> {
pub fn new(signer: &'a Signer) -> Self {
Self { signer }
}
@@ -31,7 +31,7 @@ mod seal {
fn seal(&self, _: Seal) {}
}
- impl<Signer> Access for super::WriteAs<'_, Signer> {
+ impl<Signer: crypto::Signer> Access for super::WriteAs<'_, Signer> {
fn seal(&self, _: Seal) {}
}
}
diff --git a/crates/radicle/src/cob/stream.rs b/crates/radicle/src/cob/stream.rs
index 54e2383ac..6a37f06cd 100644
--- a/crates/radicle/src/cob/stream.rs
+++ b/crates/radicle/src/cob/stream.rs
@@ -175,7 +175,7 @@ mod tests {
use serde_json as json;
use crate::cob::change::Storage as _;
- use crate::crypto::test::signer::MockSigner;
+ use crate::crypto::SigningKey;
use crate::test::arbitrary;
use crate::test::arbitrary::r#gen;
use crate::{cob, test};
@@ -186,7 +186,7 @@ mod tests {
"xyz.radicle.test".parse::<TypeName>().unwrap()
}
- fn gen_ops(repo: &git::raw::Repository, signer: &MockSigner) -> Vec<cob::Entry> {
+ fn gen_ops(repo: &git::raw::Repository, signer: &SigningKey) -> Vec<cob::Entry> {
// Number of ops
let n = r#gen::<u8>(1).clamp(1, 10);
let mut entries = Vec::with_capacity(n.into());
@@ -215,7 +215,7 @@ mod tests {
fn create_entry(
repo: &git::raw::Repository,
- signer: &MockSigner,
+ signer: &SigningKey,
contents: NonEmpty<Vec<u8>>,
parent: Option<Oid>,
) -> cob::Entry {
@@ -332,7 +332,7 @@ mod tests {
fn test_all_from() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(38);
let ops = gen_ops(&repo, &signer);
let history = CobRange {
root: ops.first().unwrap().id,
@@ -346,7 +346,7 @@ mod tests {
fn test_all_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(39);
let ops = gen_ops(&repo, &signer);
let tip = ops.last().unwrap().id;
let history = CobRange {
@@ -361,7 +361,7 @@ mod tests {
fn test_all_from_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(40);
let ops = gen_ops(&repo, &signer);
let tip = ops.last().unwrap().id;
let history = CobRange {
@@ -376,7 +376,7 @@ mod tests {
fn test_from_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(41);
let ops = gen_ops(&repo, &signer);
let history = CobRange {
root: ops.first().unwrap().id,
@@ -399,7 +399,7 @@ mod tests {
fn test_regression_from_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(42);
// Set up 3 entries that make up the COB history
let op1 = create_entry(
&repo,
diff --git a/crates/radicle/src/cob/test.rs b/crates/radicle/src/cob/test.rs
index e976cbb0a..90182ea59 100644
--- a/crates/radicle/src/cob/test.rs
+++ b/crates/radicle/src/cob/test.rs
@@ -2,20 +2,15 @@ use std::marker::PhantomData;
use std::ops::Deref;
use nonempty::NonEmpty;
-use radicle_crypto::ssh::ExtendedSignature;
use serde::{Deserialize, Serialize};
use crate::cob::op::Op;
-use crate::cob::patch::Patch;
use crate::cob::store::encoding;
use crate::cob::{Entry, History, Manifest, Timestamp, Version};
-use crate::cob::{Title, patch};
-use crate::crypto::Signer;
+use crate::crypto::ExtendedSignature;
use crate::git::{self, Oid};
-use crate::node::device::Device;
use crate::prelude::Did;
use crate::profile::env;
-use crate::storage::ReadRepository;
use crate::test::arbitrary;
use super::store::{Cob, CobWithType};
@@ -37,11 +32,11 @@ impl<T> AsRef<History> for HistoryBuilder<T> {
}
impl HistoryBuilder<thread::Thread> {
- pub fn comment<G: Signer>(
+ pub fn comment(
&mut self,
body: impl ToString,
reply_to: Option<thread::CommentId>,
- signer: &G,
+ signer: &impl crypto::Signer,
) -> Oid {
let action = thread::Action::Comment {
body: body.to_string(),
@@ -56,18 +51,21 @@ where
T: CobWithType,
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
{
- pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
+ pub fn new(
+ actions: &[T::Action],
+ time: Timestamp,
+ signer: &impl crypto::Signer,
+ ) -> HistoryBuilder<T> {
let resource = Some(arbitrary::oid());
let revision = arbitrary::oid();
let (contents, oids): (Vec<Vec<u8>>, Vec<Oid>) = actions
.iter()
- .map(|a| encoded::<T, _>(a, time, [], signer))
+ .map(|a| encoded::<T>(a, time, [], signer))
.unzip();
let contents = NonEmpty::from_vec(contents).unwrap();
let root = oids.first().unwrap();
let manifest = Manifest::new(T::type_name().clone(), Version::default());
- let signature = signer.sign(&[0]);
- let signature = ExtendedSignature::new(*signer.public_key(), signature);
+ let signature = ExtendedSignature::try_sign(signer, &[0]).unwrap();
let change = Entry {
id: *root,
signature,
@@ -96,14 +94,13 @@ where
self.history.merge(other.history);
}
- pub fn commit<G: Signer>(&mut self, action: &T::Action, signer: &G) -> crate::git::Oid {
+ pub fn commit(&mut self, action: &T::Action, signer: &impl crypto::Signer) -> crate::git::Oid {
let timestamp = self.time;
let tips = self.tips();
let revision = arbitrary::oid();
- let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer);
+ let (data, oid) = encoded::<T>(action, timestamp, tips, signer);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
- let signature = signer.sign(data.as_slice());
- let signature = ExtendedSignature::new(*signer.public_key(), signature);
+ let signature = ExtendedSignature::try_sign(signer, data.as_slice()).unwrap();
let change = Entry {
id: oid,
signature,
@@ -130,10 +127,10 @@ impl<A> Deref for HistoryBuilder<A> {
}
/// Create a new test history.
-pub fn history<T, G: Signer>(
+pub fn history<T>(
actions: &[T::Action],
time: Timestamp,
- signer: &G,
+ signer: &impl crypto::Signer,
) -> HistoryBuilder<T>
where
T: Cob + CobWithType,
@@ -142,26 +139,10 @@ where
HistoryBuilder::new(actions, time, signer)
}
-/// An object that can be used to create and sign operations.
-pub struct Actor<G> {
- pub signer: Device<G>,
-}
-
-impl<G: Default + Signer> Default for Actor<G> {
- fn default() -> Self {
- Self::new(Device::default())
- }
-}
-
-impl<G> Actor<G> {
- pub fn new(signer: Device<G>) -> Self {
- Self { signer }
- }
-}
-
-impl<G: Signer> Actor<G> {
+/// An extension trait that provides convenience methods for creating operations.
+pub trait SignerOpExt: crypto::Signer {
/// Create a new operation.
- pub fn op_with<T>(
+ fn op_with<T>(
&mut self,
actions: impl IntoIterator<Item = T::Action>,
identity: Option<Oid>,
@@ -180,7 +161,7 @@ impl<G: Signer> Actor<G> {
let oid =
crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
let id = oid.into();
- let author = *self.signer.public_key();
+ let author = self.did().into();
let actions = NonEmpty::from_vec(actions).unwrap();
let manifest = Manifest::new(T::type_name().clone(), Version::default());
let parents = vec![];
@@ -199,7 +180,7 @@ impl<G: Signer> Actor<G> {
}
/// Create a new operation.
- pub fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
+ fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
where
T: Cob + CobWithType,
T::Action: Clone + Serialize,
@@ -210,49 +191,23 @@ impl<G: Signer> Actor<G> {
self.op_with::<T>(actions, Some(identity), timestamp.into())
}
- /// Get the actor's DID.
- pub fn did(&self) -> Did {
- self.signer.public_key().into()
+ /// Get the [`Did`] corresponding to the verifying key of the signer.
+ fn did(&self) -> Did {
+ Did::from(self.verifying_key().public_key())
}
}
-impl<G: Signer> Actor<G> {
- /// Create a patch.
- pub fn patch<R: ReadRepository>(
- &mut self,
- title: Title,
- description: impl ToString,
- base: crate::git::Oid,
- oid: crate::git::Oid,
- repo: &R,
- ) -> Result<Patch, patch::Error> {
- Patch::from_root(
- self.op::<Patch>([
- patch::Action::Revision {
- description: description.to_string(),
- base,
- oid,
- resolves: Default::default(),
- },
- patch::Action::Edit {
- title,
- target: patch::MergeTarget::default(),
- },
- ]),
- repo,
- )
- }
-}
+impl<Signer> SignerOpExt for Signer where Signer: crypto::Signer {}
/// Encode an action and return its hash.
///
/// Doesn't encode in the same way as we do in production, but attempts to include the same data
/// that feeds into the hash entropy, so that changing any input will change the resulting oid.
-fn encoded<T: Cob, G: Signer>(
+fn encoded<T: Cob>(
action: &T::Action,
timestamp: Timestamp,
parents: impl IntoIterator<Item = Oid>,
- signer: &G,
+ signer: &impl crypto::Signer,
) -> (Vec<u8>, crate::git::Oid) {
use radicle_git_metadata::{
author::{Author, Time},
@@ -264,7 +219,7 @@ fn encoded<T: Cob, G: Signer>(
let parents = parents.into_iter().map(|o| o.into());
let author = Author {
name: "radicle".to_owned(),
- email: signer.public_key().to_human(),
+ email: signer.verifying_key().public_key().to_human(),
time: Time::new(timestamp.as_secs() as i64, 0),
};
let commit = CommitData::<git::raw::Oid, git::raw::Oid>::new::<_, _, OwnedTrailer>(
diff --git a/crates/radicle/src/cob/thread.rs b/crates/radicle/src/cob/thread.rs
index 6b96d808c..e15dd1289 100644
--- a/crates/radicle/src/cob/thread.rs
+++ b/crates/radicle/src/cob/thread.rs
@@ -174,8 +174,8 @@ impl<L> Comment<L> {
}
/// Return the comment author.
- pub fn author(&self) -> ActorId {
- self.author
+ pub fn author(&self) -> &ActorId {
+ &self.author
}
/// Return the comment this is a reply to. Returns nothing if this is the root comment.
@@ -624,8 +624,6 @@ pub fn unresolve<T>(
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
- use std::ops::{Deref, DerefMut};
-
use pretty_assertions::assert_eq;
use qcheck_macros::quickcheck;
@@ -633,36 +631,17 @@ mod tests {
use crate as radicle;
use crate::cob::store::Cob;
use crate::cob::test;
- use crate::crypto::Signer;
- use crate::crypto::test::signer::MockSigner;
- use crate::node::device::Device;
+ use crate::cob::test::SignerOpExt;
+ use crate::crypto::SigningKey;
use crate::profile::env;
use crate::test::arbitrary;
use crate::test::arbitrary::r#gen;
use crate::test::storage::MockRepository;
- /// An object that can be used to create and sign changes.
- pub struct Actor<G> {
- inner: cob::test::Actor<G>,
- }
-
- impl<G: Default + Signer> Default for Actor<G> {
- fn default() -> Self {
- Self {
- inner: cob::test::Actor::<G>::default(),
- }
- }
- }
-
- impl<G: Signer> Actor<G> {
- pub fn new(signer: Device<G>) -> Self {
- Self {
- inner: cob::test::Actor::new(signer),
- }
- }
-
+ /// An extension trait that provides convenience methods for handling thread operations.
+ trait SignerThreadOpExt: SignerOpExt {
/// Create a new comment.
- pub fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> {
+ fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> {
self.op::<Thread>([Action::Comment {
body: String::from(body),
reply_to,
@@ -670,12 +649,12 @@ mod tests {
}
/// Create a new redaction.
- pub fn redact(&mut self, id: CommentId) -> Op<Action> {
+ fn redact(&mut self, id: CommentId) -> Op<Action> {
self.op::<Thread>([Action::Redact { id }])
}
/// Edit a comment.
- pub fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
+ fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
self.op::<Thread>([Action::Edit {
id,
body: body.to_owned(),
@@ -683,25 +662,13 @@ mod tests {
}
}
- impl<G> Deref for Actor<G> {
- type Target = cob::test::Actor<G>;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
- }
-
- impl<G> DerefMut for Actor<G> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
- }
+ impl<T> SignerThreadOpExt for T where T: SignerOpExt {}
#[test]
fn test_redact_comment() {
let radicle::test::setup::Node { signer, .. } = radicle::test::setup::Node::default();
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::new(signer);
+ let mut alice = signer;
let a0 = alice.comment("First comment", None);
let a1 = alice.comment("Second comment", Some(a0.id()));
@@ -724,7 +691,7 @@ mod tests {
#[test]
fn test_edit_comment() {
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(49);
let repo = r#gen::<MockRepository>(1);
let c0 = alice.comment("Hello world!", None);
@@ -744,13 +711,13 @@ mod tests {
#[test]
fn test_timeline() {
- let alice = MockSigner::default();
- let bob = MockSigner::default();
- let eve = MockSigner::default();
+ let alice = SigningKey::mock(94);
+ let bob = SigningKey::mock(103);
+ let eve = SigningKey::mock(104);
let repo = r#gen::<MockRepository>(1);
let time = env::local_time();
- let mut a = test::history::<Thread, _>(
+ let mut a = test::history::<Thread>(
&[Action::Comment {
body: "Thread root".to_owned(),
reply_to: None,
@@ -813,11 +780,12 @@ mod tests {
#[test]
fn test_duplicate_comments() {
let repo = r#gen::<MockRepository>(1);
- let alice = MockSigner::default();
- let bob = MockSigner::default();
+ let alice = SigningKey::mock(94);
+ let bob = SigningKey::mock(103);
+ let _eve = SigningKey::mock(104);
let time = env::local_time();
- let mut a = test::history::<Thread, _>(
+ let mut a = test::history::<Thread>(
&[Action::Comment {
body: "Thread root".to_owned(),
reply_to: None,
@@ -857,11 +825,12 @@ mod tests {
#[quickcheck]
fn prop_ordering(timestamp: u64) {
let repo = r#gen::<MockRepository>(1);
- let alice = MockSigner::default();
- let bob = MockSigner::default();
+ let alice = SigningKey::mock(94);
+ let bob = SigningKey::mock(103);
+ let _eve = SigningKey::mock(104);
let timestamp = Timestamp::from_secs(timestamp);
- let h0 = test::history::<Thread, _>(
+ let h0 = test::history::<Thread>(
&[Action::Comment {
body: "Thread root".to_owned(),
reply_to: None,
@@ -916,7 +885,7 @@ mod tests {
#[test]
fn test_comment_redact_missing() {
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(94);
let mut t = Thread::default();
let id = arbitrary::entry_id();
@@ -926,7 +895,7 @@ mod tests {
#[test]
fn test_comment_edit_missing() {
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(94);
let mut t = Thread::default();
let id = arbitrary::entry_id();
@@ -936,7 +905,7 @@ mod tests {
#[test]
fn test_comment_edit_redacted() {
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(94);
let a1 = alice.comment("Hi", None);
let a2 = alice.redact(a1.id);
diff --git a/crates/radicle/src/git/canonical.rs b/crates/radicle/src/git/canonical.rs
index ce49a0e48..ea3f3db51 100644
--- a/crates/radicle/src/git/canonical.rs
+++ b/crates/radicle/src/git/canonical.rs
@@ -507,8 +507,8 @@ mod tests {
use super::*;
use crate::assert_matches;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::git;
- use crate::node::device::Device;
use crate::test::fixtures;
/// Test helper to construct a Canonical and get the quorum
@@ -521,7 +521,7 @@ mod tests {
let mut delegates = Vec::new();
for (i, head) in heads.iter().enumerate() {
- let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
+ let signer = SigningKey::mock(i);
let did = Did::from(signer.public_key());
delegates.push(did);
let ns = git::fmt::Component::from(signer.public_key());
diff --git a/crates/radicle/src/git/canonical/rules/test.rs b/crates/radicle/src/git/canonical/rules/test.rs
index 07887b11b..9931d1fac 100644
--- a/crates/radicle/src/git/canonical/rules/test.rs
+++ b/crates/radicle/src/git/canonical/rules/test.rs
@@ -6,12 +6,11 @@ use std::collections::BTreeMap;
use nonempty::nonempty;
use crate::Storage;
-use crate::crypto::{Signer, test::signer::MockSigner};
+use crate::crypto::{Signer as _, SigningKey};
use crate::git;
use crate::git::fmt::qualified_pattern;
use crate::identity::Visibility;
use crate::identity::doc::Doc;
-use crate::node::device::Device;
use crate::rad;
use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH, SIGREFS_PARENT};
use crate::storage::{ReadStorage, git::transport};
@@ -295,23 +294,25 @@ fn rule_validate_failures() {
Err(ValidationError::Threshold(_))
));
- let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
+ let delegates = NonEmpty::from_vec(
+ arbitrary::set::<Did>(256..=256)
+ .into_iter()
+ .collect::<Vec<_>>(),
+ )
+ .unwrap();
+ assert_eq!(delegates.len(), 256);
assert!(matches!(
Rule::new(delegates.into(), 1).validate(&mut || resolve_from_doc(&doc)),
Err(ValidationError::Delegates(_))
));
- let delegates = nonempty![
- did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
- did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
- ];
+ let delegate = Did::from(SigningKey::mock(22).public_key());
+
+ let delegates = nonempty![delegate, delegate];
+ assert_eq!(delegates.len(), 2);
+
let expected = Rule {
- allow: ResolvedDelegates::Set(
- doc::Delegates::new(nonempty![did(
- "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
- )])
- .unwrap(),
- ),
+ allow: ResolvedDelegates::Set(doc::Delegates::new(nonempty![delegate]).unwrap()),
threshold: doc::Threshold::MIN,
extensions: json::Map::new(),
};
@@ -357,8 +358,8 @@ fn canonical() {
transport::local::register(storage.clone());
- let delegate = Device::mock_from_seed([0xff; 32]);
- let contributor = MockSigner::from_seed([0xfe; 32]);
+ let delegate = SigningKey::mock(0xff);
+ let contributor = SigningKey::mock(0xfe);
let (repo, head) = fixtures::repository(tempdir.path().join("working"));
let (rid, doc, _) = rad::init(
&repo,
diff --git a/crates/radicle/src/git/raw.rs b/crates/radicle/src/git/raw.rs
index 68f589ed0..4fc73ce3e 100644
--- a/crates/radicle/src/git/raw.rs
+++ b/crates/radicle/src/git/raw.rs
@@ -5,9 +5,7 @@
// Re-exports created by manually scanning the `heartwood` workspace on 2025-10-04.
// Re-exports that are only used within this crate.
-pub(crate) use git2::{
- AutotagOption, Blob, FetchOptions, FetchPrune, Object, Revwalk, Sort, message_trailers_strs,
-};
+pub(crate) use git2::{AutotagOption, Blob, FetchOptions, FetchPrune, Object, Revwalk, Sort};
#[cfg(unix)]
pub(crate) use git2::Config;
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 7230ff2bd..a6147124b 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -26,11 +26,10 @@ use crate::git::fmt::RefString;
use crate::git::raw::ErrorExt as _;
use crate::identity::crefs;
use crate::identity::{Did, project::Project};
-use crate::node::device::Device;
use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};
-pub use crypto::PublicKey;
+pub use crypto::{PublicKey, VerifyingKey};
pub use radicle_core::repo::*;
use super::CanonicalRefs;
@@ -959,13 +958,16 @@ impl Doc {
signature: &Signature,
blob: Oid,
) -> Result<(), PublicKey> {
+ use crypto::signature::Verifier as _;
+
if !self.is_delegate(&key.into()) {
return Err(*key);
}
- if key.verify(AsRef::<[u8]>::as_ref(&blob), signature).is_err() {
- return Err(*key);
- }
- Ok(())
+
+ VerifyingKey::try_from(key)
+ .map_err(|_| *key)?
+ .verify(AsRef::<[u8]>::as_ref(&blob), signature)
+ .map_err(|_| *key)
}
/// Check the provided `votes` passes the [`Doc::majority`].
@@ -1002,10 +1004,10 @@ impl Doc {
/// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
/// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
- pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ pub fn sign(
+ &self,
+ signer: &impl crypto::Signer,
+ ) -> Result<(git::Oid, Vec<u8>, Signature), DocError> {
let (oid, bytes) = self.encode()?;
let sig = signer.sign(oid.as_ref());
@@ -1013,10 +1015,7 @@ impl Doc {
}
/// Similar to [`Doc::sign`], but only returning the [`Signature`].
- pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ pub fn signature_of(&self, signer: &impl crypto::Signer) -> Result<Signature, DocError> {
let (_, _, sig) = self.sign(signer)?;
Ok(sig)
@@ -1037,21 +1036,15 @@ impl Doc {
/// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
/// document.
- pub fn init<G>(
+ pub fn init(
&self,
repo: &storage::git::Repository,
- signer: &Device<G>,
- ) -> Result<git::Oid, RepositoryError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> Result<git::Oid, RepositoryError> {
let cob = identity::Identity::initialize(self, repo, signer)?;
- let id_ref = git::refs::storage::id(signer.public_key());
- let cob_ref = git::refs::storage::cob(
- signer.public_key(),
- &crate::cob::identity::TYPENAME,
- &cob.id,
- );
+ let public_key = signer.public_key();
+ let id_ref = git::refs::storage::id(public_key);
+ let cob_ref = git::refs::storage::cob(public_key, &crate::cob::identity::TYPENAME, &cob.id);
// Set `…/refs/rad/id` -> `…/refs/cobs/xyz.radicle.id/<id>`
repo.backend.reference_symbolic(
id_ref.as_str(),
@@ -1129,6 +1122,7 @@ impl GetRawCanonicalRefs for RawDoc {}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
+ use crypto::{Signer as _, SigningKey};
use serde_json::json;
use crate::assert_matches;
@@ -1145,7 +1139,7 @@ mod test {
#[test]
fn test_duplicate_dids() {
- let delegate = Device::mock_from_seed([0xff; 32]);
+ let delegate = SigningKey::mock(usize::MAX);
let did = Did::from(delegate.public_key());
let mut doc = RawDoc::new(r#gen::<Project>(1), vec![did], 1, Visibility::Public);
doc.delegate(did);
@@ -1157,7 +1151,9 @@ mod test {
#[test]
fn test_max_delegates() {
// Generate more than the max delegates
- let delegates = (0..MAX_DELEGATES + 1).map(r#gen).collect::<Vec<Did>>();
+ let delegates = arbitrary::set::<Did>(MAX_DELEGATES + 1..=MAX_DELEGATES + 1)
+ .into_iter()
+ .collect::<Vec<_>>();
// A document with max delegates will be fine
let doc = RawDoc::new(
@@ -1280,7 +1276,7 @@ mod test {
transport::local::register(storage.clone());
- let delegate = Device::mock_from_seed([0xff; 32]);
+ let delegate = SigningKey::mock(usize::MAX);
let (repo, _) = fixtures::repository(tempdir.path().join("working"));
let (id, _, _) = rad::init(
&repo,
@@ -1331,7 +1327,7 @@ mod test {
let (working, _) = fixtures::repository(tempdir.path().join("working"));
- let delegate = Device::mock_from_seed([0xff; 32]);
+ let delegate = SigningKey::mock(usize::MAX);
let (rid, doc, _) = rad::init(
&working,
"heartwood".try_into().unwrap(),
diff --git a/crates/radicle/src/identity/project.rs b/crates/radicle/src/identity/project.rs
index 6509e7de7..c530870e0 100644
--- a/crates/radicle/src/identity/project.rs
+++ b/crates/radicle/src/identity/project.rs
@@ -12,7 +12,7 @@ use crate::git::{fmt::Qualified, refs::branch};
use crate::identity::doc;
use crate::identity::doc::Payload;
-pub use crypto::PublicKey;
+pub use crypto::VerifyingKey;
/// A project-related error.
#[derive(Debug, Error)]
diff --git a/crates/radicle/src/node.rs b/crates/radicle/src/node.rs
index 7503522a8..18b7f2679 100644
--- a/crates/radicle/src/node.rs
+++ b/crates/radicle/src/node.rs
@@ -6,7 +6,6 @@ use address::AddressType;
pub mod command;
pub mod config;
pub mod db;
-pub mod device;
pub mod events;
pub mod notifications;
pub mod policy;
diff --git a/crates/radicle/src/node/device.rs b/crates/radicle/src/node/device.rs
deleted file mode 100644
index 76a715757..000000000
--- a/crates/radicle/src/node/device.rs
+++ /dev/null
@@ -1,220 +0,0 @@
-use std::fmt;
-use std::ops::Deref;
-
-use crypto::{
- Signature,
- signature::{Keypair, KeypairRef, Signer, Verifier},
- ssh::ExtendedSignature,
-};
-
-use crate::crypto;
-
-use super::NodeId;
-
-/// A `Device` identifies the local node through its [`NodeId`], and carries its
-/// signing mechanism.
-///
-/// The signing mechanism is for node specific cryptography, e.g. signing
-/// `rad/sigrefs`, COB commits, node messages, etc.
-///
-/// Note that a `Device` can create [`Signature`]s and [`ExtendedSignature`]s.
-/// It can achieve this as long as `S` implements `Signer<Signature>`.
-#[derive(Clone)]
-pub struct Device<S> {
- node: NodeId,
- signer: S,
-}
-
-impl<S> fmt::Debug for Device<S> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Device")
- .field("node", &self.node.to_human())
- .finish()
- }
-}
-
-impl<S: crypto::Signer + Default> Default for Device<S> {
- fn default() -> Self {
- Self::from(S::default())
- }
-}
-
-impl<S: crypto::Signer> Device<S> {
- /// Construct a new `Device`.
- pub fn new(signer: S) -> Self {
- Self {
- node: *signer.public_key(),
- signer,
- }
- }
-}
-
-impl<S> Device<S> {
- /// Return the [`NodeId`] of the `Device.`
- pub fn node_id(&self) -> &NodeId {
- &self.node
- }
-
- /// Return the [`crypto::PublicKey`] of the `Device.`
- pub fn public_key(&self) -> &crypto::PublicKey {
- &self.node
- }
-
- /// Convert the `Device` into its signer.
- ///
- /// This consumes the `Device`.
- pub fn into_inner(self) -> S {
- self.signer
- }
-}
-
-#[cfg(any(test, feature = "test"))]
-impl Device<crypto::test::signer::MockSigner> {
- /// Construct a `Device` using a default `MockSigner` as the device signer.
- pub fn mock() -> Self {
- Device::from(crypto::test::signer::MockSigner::default())
- }
-
- /// Construct a `Device`, constructing an RNG'd `MockSigner` for the signer.
- pub fn mock_rng(rng: &mut fastrand::Rng) -> Self {
- Device::from(crypto::test::signer::MockSigner::new(rng))
- }
-
- /// Construct a `Device`, constructing a seeded `MockSigner` for the signer.
- pub fn mock_from_seed(seed: [u8; 32]) -> Self {
- Device::from(crypto::test::signer::MockSigner::from_seed(seed))
- }
-}
-
-impl<S: BoxableSigner + 'static> Device<S> {
- /// Construct a [`BoxedDevice`] from a given `Device`.
- pub fn boxed(self) -> BoxedDevice {
- BoxedDevice(Device {
- node: self.node,
- signer: BoxedSigner(Box::new(self.signer)),
- })
- }
-}
-
-impl<S> AsRef<NodeId> for Device<S> {
- fn as_ref(&self) -> &NodeId {
- &self.node
- }
-}
-
-impl<S> KeypairRef for Device<S> {
- type VerifyingKey = NodeId;
-}
-
-impl<S> Verifier<Signature> for Device<S> {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> {
- self.node
- .verify(msg, signature)
- .map_err(crypto::signature::Error::from_source)
- }
-}
-
-impl<S: crypto::Signer> From<S> for Device<S> {
- fn from(signer: S) -> Self {
- Self {
- node: *signer.public_key(),
- signer,
- }
- }
-}
-
-impl<S: crypto::Signer + Clone> From<&S> for Device<S> {
- fn from(signer: &S) -> Self {
- Self::from(signer.clone())
- }
-}
-
-impl<S: Signer<Signature>> Signer<Signature> for Device<S> {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
- self.signer.try_sign(msg)
- }
-}
-
-impl<S: Signer<Signature>> Signer<ExtendedSignature> for Device<S> {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, crypto::signature::Error> {
- Ok(ExtendedSignature {
- key: *self.public_key(),
- sig: self.signer.try_sign(msg)?,
- })
- }
-}
-
-pub trait BoxableSigner: Signer<Signature> + Keypair<VerifyingKey = crypto::PublicKey> {}
-
-impl<S: Signer<Signature> + Keypair<VerifyingKey = crypto::PublicKey>> BoxableSigner for S {}
-
-/// A `Signer<Signature>` that is packed in a [`Box`] for dynamic dispatch.
-pub struct BoxedSigner(Box<dyn BoxableSigner + 'static>);
-
-impl Signer<Signature> for BoxedSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
- self.0.try_sign(msg)
- }
-}
-
-impl Keypair for BoxedSigner {
- type VerifyingKey = crypto::PublicKey;
-
- fn verifying_key(&self) -> Self::VerifyingKey {
- self.0.verifying_key()
- }
-}
-
-/// A `Device` where the signer is a dynamic `Signer<Signature>`, in the form of
-/// a [`BoxedSigner`].
-///
-/// This can be constructed via [`Device::boxed`].
-pub struct BoxedDevice(Device<BoxedSigner>);
-
-impl AsRef<Device<BoxedSigner>> for BoxedDevice {
- fn as_ref(&self) -> &Device<BoxedSigner> {
- &self.0
- }
-}
-
-impl Deref for BoxedDevice {
- type Target = Device<BoxedSigner>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl Signer<Signature> for BoxedDevice {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
- self.0.signer.try_sign(msg)
- }
-}
-
-impl Signer<ExtendedSignature> for BoxedDevice {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, crypto::signature::Error> {
- Ok(ExtendedSignature {
- key: *self.0.public_key(),
- sig: self.0.signer.try_sign(msg)?,
- })
- }
-}
-
-impl AsRef<crypto::PublicKey> for BoxedDevice {
- fn as_ref(&self) -> &crypto::PublicKey {
- &self.0.node
- }
-}
-
-impl KeypairRef for BoxedDevice {
- type VerifyingKey = crypto::PublicKey;
-}
-
-impl Verifier<Signature> for BoxedDevice {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> {
- self.0
- .node
- .verify(msg, signature)
- .map_err(crypto::signature::Error::from_source)
- }
-}
diff --git a/crates/radicle/src/node/routing.rs b/crates/radicle/src/node/routing.rs
index 008477263..d3390d5e6 100644
--- a/crates/radicle/src/node/routing.rs
+++ b/crates/radicle/src/node/routing.rs
@@ -473,7 +473,7 @@ mod test {
let mut rng = fastrand::Rng::new();
let now = LocalTime::now();
let ids = arbitrary::vec::<RepoId>(10);
- let nodes = arbitrary::vec::<NodeId>(10);
+ let nodes = arbitrary::vec_distinct::<NodeId>(10..=10);
let mut db = database(":memory:");
for node in &nodes {
diff --git a/crates/radicle/src/node/sync/announce.rs b/crates/radicle/src/node/sync/announce.rs
index 602a961a4..12b7d7689 100644
--- a/crates/radicle/src/node/sync/announce.rs
+++ b/crates/radicle/src/node/sync/announce.rs
@@ -840,10 +840,15 @@ mod test {
#[test]
fn announcer_reached_preferred_seeds() {
- let local = arbitrary::r#gen::<NodeId>(0);
let seeds = arbitrary::set::<NodeId>(10..=10);
- let unsynced = seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
- let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
+ let local = seeds.iter().next().copied().unwrap();
+ let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
+ let preferred_seeds = seeds
+ .iter()
+ .skip(1)
+ .take(2)
+ .copied()
+ .collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(11),
@@ -1555,7 +1560,7 @@ mod test {
#[test]
fn synced_with_local_node_is_ignored() {
let local = arbitrary::r#gen::<NodeId>(0);
- let unsynced = arbitrary::set::<NodeId>(3..=3).into_iter().collect();
+ let unsynced = BTreeSet::from_iter(arbitrary::set::<NodeId>(3..=3));
let config = AnnouncerConfig::public(
local,
diff --git a/crates/radicle/src/node/sync/fetch.rs b/crates/radicle/src/node/sync/fetch.rs
index ff26c8c79..c8789a0ce 100644
--- a/crates/radicle/src/node/sync/fetch.rs
+++ b/crates/radicle/src/node/sync/fetch.rs
@@ -550,7 +550,18 @@ mod test {
let seeds = arbitrary::set::<NodeId>(3..=6)
.into_iter()
.collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ // Make sure that `extra_candidates` does not contain any of the preferred seeds.
+ let extra_candidates = loop {
+ let extra_candidates = arbitrary::set::<NodeId>(3..=3)
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+
+ if seeds.intersection(&extra_candidates).count() == 0 {
+ break extra_candidates;
+ }
+ };
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
@@ -574,10 +585,11 @@ mod test {
#[test]
fn ignores_duplicates_and_local_node() {
- let local = arbitrary::r#gen::<NodeId>(0);
let replicas = ReplicationFactor::default();
- let bob = arbitrary::r#gen::<NodeId>(1);
- let eve = arbitrary::r#gen::<NodeId>(2);
+
+ // Ensure that the three nodes in this test have pairwise distinct IDs.
+ let [local, bob, eve] = arbitrary::vec::<NodeId>(3).try_into().unwrap();
+
let seeds = [bob].into_iter().collect::<BTreeSet<_>>();
let extra_candidates = vec![bob, local, eve];
let config = FetcherConfig::public(seeds.clone(), replicas, local)
@@ -631,7 +643,18 @@ mod test {
let seeds = arbitrary::set::<NodeId>(3..=3)
.into_iter()
.collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ // Make sure that `extra_candidates` does not contain any of the preferred seeds.
+ let extra_candidates = loop {
+ let extra_candidates = arbitrary::set::<NodeId>(3..=3)
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+
+ if seeds.intersection(&extra_candidates).count() == 0 {
+ break extra_candidates;
+ }
+ };
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
@@ -673,12 +696,26 @@ mod test {
let seeds = arbitrary::set::<NodeId>(3..=3)
.into_iter()
.collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ // Make sure that `extra_candidates` does not contain any of the preferred seeds.
+ let extra_candidates = loop {
+ let extra_candidates = arbitrary::set::<NodeId>(3..=3)
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+
+ if seeds.intersection(&extra_candidates).count() == 0 {
+ break extra_candidates;
+ }
+ };
+
+ assert_eq!(extra_candidates.len(), replicas.lower_bound());
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
let mut fetcher = Fetcher::new(config).expect("fetcher should be constructed correctly");
let mut result = Vec::with_capacity(extra_candidates.len());
+
let expected = extra_candidates
.clone()
.into_iter()
diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs
index 403bd5706..f20991935 100644
--- a/crates/radicle/src/profile.rs
+++ b/crates/radicle/src/profile.rs
@@ -14,6 +14,9 @@
pub mod config;
pub use config::{Config, WriteError};
+mod signer;
+pub use signer::Signer;
+
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::{fs, io};
@@ -23,10 +26,9 @@ use thiserror::Error;
use crate::cob::migrate;
use crate::cob::store::access::{ReadOnly, WriteAs};
-use crate::crypto::PublicKey;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{Keystore, Passphrase, keystore};
-use crate::node::device::{BoxedDevice, Device};
+use crate::crypto::{PublicKey, Seed};
use crate::node::policy::config::store::Read;
use crate::node::{Alias, AliasStore, Handle as _, Node, notifications, policy, policy::Scope};
use crate::prelude::{Did, NodeId, RepoId};
@@ -205,32 +207,19 @@ pub enum Error {
#[derive(Debug, Error)]
pub enum SignerError {
#[error(transparent)]
- MemorySigner(#[from] keystore::MemorySignerError),
+ LoadError(#[from] crypto::LoadError),
#[error(transparent)]
- Agent(#[from] crate::crypto::ssh::agent::AgentError),
-
- #[error("Radicle key `{0}` is not registered; run `rad auth` to register it with ssh-agent")]
- KeyNotRegistered(PublicKey),
+ Agent(#[from] crate::crypto::ssh::agent::IntoSignerError),
#[error(transparent)]
Keystore(#[from] keystore::Error),
- #[error("error connecting to ssh-agent: {source}")]
- AgentConnection {
- source: crate::crypto::ssh::agent::ConnectError,
- },
-}
+ #[error("error connecting to ssh-agent: {0}")]
+ AgentConnection(#[from] crate::crypto::ssh::agent::ConnectError),
-impl SignerError {
- /// Some signer errors are potentially recoverable by prompting the user
- /// for a password.
- pub fn prompt_for_passphrase(&self) -> bool {
- matches!(
- self,
- Self::AgentConnection { .. } | Self::KeyNotRegistered(_)
- )
- }
+ #[error("public key is invalid: {0}")]
+ InvalidPublicKey(#[source] crypto::signature::Error),
}
#[derive(Debug, Clone)]
@@ -247,7 +236,7 @@ impl Profile {
home: Home,
alias: Alias,
passphrase: Option<Passphrase>,
- seed: crypto::Seed,
+ seed: Seed,
) -> Result<Self, Error> {
let keystore = Keystore::new(&home.keys());
let public_key = keystore.init("radicle", passphrase, seed)?;
@@ -333,24 +322,36 @@ impl Profile {
Did::from(self.public_key)
}
- pub fn signer(&self) -> Result<BoxedDevice, SignerError> {
- if !self.keystore.is_encrypted()? {
- let signer = keystore::MemorySigner::load(&self.keystore, None)?;
- return Ok(Device::from(signer).boxed());
- }
-
- if let Some(passphrase) = env::passphrase() {
- let signer = keystore::MemorySigner::load(&self.keystore, Some(passphrase))?;
- return Ok(Device::from(signer).boxed());
- }
+ pub fn signer(&self) -> Result<Signer, SignerError> {
+ let key = {
+ // First, check whether the key is encrypted.
+ if self.keystore.is_encrypted()? {
+ // The key is encrypted, so we need a passphrase to load it.
+ if let Some(passphrase) = env::passphrase() {
+ // We have a passphrase from the environment,
+ // so we use it to load the key.
+ (true, Some(passphrase))
+ } else {
+ // We do not have a passphrase, so we cannot load the key.
+ (false, None)
+ }
+ } else {
+ // The key is not encrypted, so we load it without a passphrase.
+ (true, None)
+ }
+ };
- let agent = Agent::connect().map_err(|source| SignerError::AgentConnection { source })?;
- let signer = agent.signer(self.public_key);
- if signer.is_ready()? {
- Ok(Device::from(signer).boxed())
+ Ok(if let (true, passphrase) = key {
+ Signer::Key(crypto::SigningKey::load(&self.keystore, passphrase)?)
} else {
- Err(SignerError::KeyNotRegistered(self.public_key))
- }
+ Signer::Agent(
+ Agent::connect()?.into_signer(
+ self.id()
+ .try_into()
+ .map_err(SignerError::InvalidPublicKey)?,
+ )?,
+ )
+ })
}
/// Get Radicle home.
@@ -797,7 +798,7 @@ impl Home {
}
/// Return a read-write handle for the issues cache.
- pub fn issues_mut<'a, 'b, Repo, Signer>(
+ pub fn issues_mut<'a, 'b, Repo, Signer: crypto::Signer>(
&self,
repository: &'a Repo,
signer: &'b Signer,
@@ -830,7 +831,7 @@ impl Home {
}
/// Return a read-write handle for the patches cache.
- pub fn patches_mut<'a, 'b, Repo, Signer>(
+ pub fn patches_mut<'a, 'b, Repo, Signer: crypto::Signer>(
&self,
repository: &'a Repo,
signer: &'b Signer,
diff --git a/crates/radicle/src/profile/signer.rs b/crates/radicle/src/profile/signer.rs
new file mode 100644
index 000000000..d1f2e0226
--- /dev/null
+++ b/crates/radicle/src/profile/signer.rs
@@ -0,0 +1,41 @@
+use crypto::{PublicKey, Signature, SigningKey, VerifyingKey, signature, ssh::agent::AgentSigner};
+
+/// Wraps well-known implementations of [`signature::Signer`] and
+/// [`signature::Keypair`].
+///
+/// This is used to abstract over the different ways of signing (references and
+/// COB operations), such as using a [`SigningKey`] directly (usually loaded via
+/// [`crypto::ssh::Keystore`]) or SSH Agent (via [`AgentSigner`]).
+pub enum Signer {
+ Key(SigningKey),
+ Agent(AgentSigner),
+}
+
+impl signature::Signer<Signature> for Signer {
+ fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+ match self {
+ Signer::Key(key) => key.try_sign(msg),
+ Signer::Agent(agent) => agent.try_sign(msg),
+ }
+ }
+}
+
+impl signature::Keypair for Signer {
+ type VerifyingKey = VerifyingKey;
+
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ match self {
+ Signer::Key(key) => key.verifying_key(),
+ Signer::Agent(agent) => agent.verifying_key(),
+ }
+ }
+}
+
+impl AsRef<PublicKey> for Signer {
+ fn as_ref(&self) -> &PublicKey {
+ match self {
+ Signer::Key(key) => key.as_ref(),
+ Signer::Agent(agent) => agent.as_ref(),
+ }
+ }
+}
diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs
index 74d89fd8f..91ca24356 100644
--- a/crates/radicle/src/rad.rs
+++ b/crates/radicle/src/rad.rs
@@ -11,7 +11,6 @@ use crate::git::BranchName;
use crate::identity::doc;
use crate::identity::doc::{DocError, RepoId, Visibility};
use crate::identity::project::{Project, ProjectName};
-use crate::node::device::Device;
use crate::storage::RepositoryError;
use crate::storage::git::Repository;
use crate::storage::git::transport;
@@ -46,19 +45,15 @@ pub enum InitError {
}
/// Initialize a new Radicle project from a git repository.
-pub fn init<G, S>(
+pub fn init(
repo: &git::raw::Repository,
name: ProjectName,
description: &str,
default_branch: BranchName,
visibility: Visibility,
- signer: &Device<G>,
- storage: S,
-) -> Result<(RepoId, identity::Doc, SignedRefs), InitError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
- S: WriteStorage,
-{
+ signer: &impl crypto::Signer,
+ storage: &impl WriteStorage,
+) -> Result<(RepoId, identity::Doc, SignedRefs), InitError> {
// TODO: Better error when project id already exists in storage, but remote doesn't.
let delegate: identity::Did = signer.public_key().into();
let proj = Project::new(
@@ -76,7 +71,7 @@ where
})?;
let doc = identity::Doc::initial(proj, delegate, visibility);
- let (project, identity) = Repository::init(&doc, &storage, signer)?;
+ let (project, identity) = Repository::init(&doc, storage, signer)?;
let url = git::Url::from(project.id);
match init_configure(repo, &project, &default_branch, &url, identity, signer) {
@@ -95,17 +90,14 @@ where
}
}
-fn init_configure<G>(
+fn init_configure(
repo: &git::raw::Repository,
stored: &Repository,
default_branch: &BranchName,
url: &git::Url,
identity: git::Oid,
- signer: &Device<G>,
-) -> Result<SignedRefs, InitError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
-{
+ signer: &impl crypto::Signer,
+) -> Result<SignedRefs, InitError> {
let pk = signer.public_key();
git::configure_repository(repo)?;
@@ -166,16 +158,12 @@ pub enum ForkError {
/// Create a local tree for an existing project, from an existing remote.
#[cfg(any(test, feature = "test"))]
-pub fn fork_remote<G, S>(
+pub fn fork_remote(
proj: RepoId,
remote: &RemoteId,
- signer: &Device<G>,
- storage: S,
-) -> Result<(), ForkError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
- S: storage::WriteStorage,
-{
+ signer: &impl crypto::Signer,
+ storage: impl storage::WriteStorage,
+) -> Result<(), ForkError> {
// TODO: Copy tags over?
// Creates or copies the following references:
@@ -205,11 +193,11 @@ where
Ok(())
}
-pub fn fork<G, S>(rid: RepoId, signer: &Device<G>, storage: &S) -> Result<(), ForkError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
- S: storage::WriteStorage,
-{
+pub fn fork(
+ rid: RepoId,
+ signer: &impl crypto::Signer,
+ storage: &impl storage::WriteStorage,
+) -> Result<(), ForkError> {
let me = signer.public_key();
let repository = storage.repository_mut(rid)?;
let (canonical_branch, canonical_head) = repository.head()?;
@@ -510,6 +498,7 @@ mod tests {
use pretty_assertions::assert_eq;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::identity::Did;
use crate::storage::git::Storage;
use crate::storage::git::transport;
@@ -522,8 +511,8 @@ mod tests {
#[test]
fn test_init() {
let tempdir = tempfile::tempdir().unwrap();
- let signer = Device::mock();
- let public_key = *signer.public_key();
+ let signer = SigningKey::mock(73);
+ let public_key = signer.public_key();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
@@ -564,7 +553,7 @@ mod tests {
.unwrap(),
);
- assert_eq!(remotes[&public_key].refs.refs(), refs.refs());
+ assert_eq!(remotes[public_key].refs.refs(), refs.refs());
assert_eq!(project.name(), "acme");
assert_eq!(project.description(), "Acme's repo");
assert_eq!(project.default_branch(), &git::fmt::refname!("master"));
@@ -575,8 +564,8 @@ mod tests {
fn test_fork() {
let mut rng = fastrand::Rng::new();
let tempdir = tempfile::tempdir().unwrap();
- let alice = Device::mock_rng(&mut rng);
- let bob = Device::mock_rng(&mut rng);
+ let alice = SigningKey::mock(rng.usize(..));
+ let bob = SigningKey::mock(rng.usize(..));
let bob_id = bob.public_key();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
@@ -613,7 +602,7 @@ mod tests {
#[test]
fn test_checkout() {
let tempdir = tempfile::tempdir().unwrap();
- let signer = Device::mock();
+ let signer = SigningKey::mock(73);
let remote_id = signer.public_key();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index 99c99a69c..a11bb9fe0 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -728,20 +728,12 @@ pub trait WriteRepository: ReadRepository + SignRepository {
/// Allows signing refs.
pub trait SignRepository {
/// Sign the repository's refs under the `refs/rad/sigrefs` branch.
- fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>;
+ fn sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError>;
/// Sign the repository's refs under the `refs/rad/sigrefs` branch, even if unchanged.
///
/// Most users will prefer [`Self::sign_refs`].
- fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>;
+ fn force_sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError>;
}
impl<T, S> ReadStorage for T
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index d130e51d7..956a013fc 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -5,7 +5,7 @@ pub mod transport;
pub mod temp;
pub use temp::TempRepository;
-use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::collections::{BTreeMap, BTreeSet};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
@@ -16,7 +16,6 @@ use crate::git::raw::ErrorExt as _;
use crate::identity::doc::DocError;
use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
-use crate::node::device::Device;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs};
use crate::storage::{
ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
@@ -522,15 +521,11 @@ impl Repository {
}
/// Create the repository's identity branch.
- pub fn init<G, S>(
+ pub fn init(
doc: &Doc,
- storage: &S,
- signer: &Device<G>,
- ) -> Result<(Self, crate::git::Oid), RepositoryError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- S: WriteStorage,
- {
+ storage: &impl WriteStorage,
+ signer: &impl crypto::Signer,
+ ) -> Result<(Self, crate::git::Oid), RepositoryError> {
let (doc_oid, doc_bytes) = doc.encode()?;
let id = RepoId::from(doc_oid);
let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
@@ -1032,52 +1027,37 @@ impl WriteRepository for Repository {
}
impl SignRepository for Repository {
- fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ fn sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError> {
self.sign_refs_with(signer, false)
}
- fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ fn force_sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError> {
self.sign_refs_with(signer, true)
}
}
impl Repository {
- fn sign_refs_with<Signer>(
+ fn sign_refs_with(
&self,
- signer: &Signer,
+ signer: &impl crypto::Signer,
force: bool,
- ) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
- let remote = signer.verifying_key();
+ ) -> Result<SignedRefs, RepositoryError> {
+ let remote = signer.public_key();
// Ensure the root reference is set, which is checked during sigref verification.
if self
- .reference_oid(&remote, &git::refs::storage::IDENTITY_ROOT)
+ .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT)
.is_err()
{
- self.set_remote_identity_root(&remote)?;
+ self.set_remote_identity_root(remote)?;
}
- let committer = refs::sigrefs::git::Committer::from_env_or_now(&remote);
+ let committer = refs::sigrefs::git::Committer::from_env_or_now(remote);
- let refs = self.references_of(&remote)?;
+ let refs = self.references_of(remote)?;
let signed = if force {
- refs.force_save(remote, committer, self, signer)?
+ refs.force_save(*remote, committer, self, signer)?
} else {
- refs.save(remote, committer, self, signer)?
+ refs.save(*remote, committer, self, signer)?
};
Ok(signed)
@@ -1137,48 +1117,6 @@ impl sigrefs::git::reference::Writer for Repository {
}
}
-pub mod trailers {
- use std::str::FromStr;
-
- use thiserror::Error;
-
- use super::*;
- use crypto::{PublicKey, PublicKeyError};
- use crypto::{Signature, SignatureError};
-
- pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
-
- #[derive(Error, Debug)]
- pub enum Error {
- #[error("invalid format for signature trailer")]
- SignatureTrailerFormat,
- #[error("invalid public key in signature trailer")]
- PublicKey(#[from] PublicKeyError),
- #[error("invalid signature in trailer")]
- Signature(#[from] SignatureError),
- }
-
- pub fn parse_signatures(msg: &str) -> Result<HashMap<PublicKey, Signature>, Error> {
- let trailers =
- git::raw::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
- let mut signatures = HashMap::with_capacity(trailers.len());
-
- for (key, val) in trailers.iter() {
- if key == SIGNATURE_TRAILER {
- if let Some((pk, sig)) = val.split_once(' ') {
- let pk = PublicKey::from_str(pk)?;
- let sig = Signature::from_str(sig)?;
-
- signatures.insert(pk, sig);
- } else {
- return Err(Error::SignatureTrailerFormat);
- }
- }
- }
- Ok(signatures)
- }
-}
-
pub mod paths {
use std::path::PathBuf;
@@ -1197,13 +1135,14 @@ mod tests {
use super::*;
use crate::git;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::storage::{ReadRepository, ReadStorage};
use crate::test::fixtures;
#[test]
fn test_references_of() {
let tmp = tempfile::tempdir().unwrap();
- let signer = Device::mock();
+ let signer = SigningKey::mock(54);
let storage = Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
@@ -1237,10 +1176,9 @@ mod tests {
#[test]
fn test_sign_refs() {
let tmp = tempfile::tempdir().unwrap();
- let mut rng = fastrand::Rng::new();
- let signer = Device::mock_rng(&mut rng);
+ let signer = SigningKey::mock(0xb4);
let storage = Storage::open(tmp.path(), fixtures::user()).unwrap();
- let alice = *signer.public_key();
+ let alice = signer.public_key();
let (rid, _, working, _) =
fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
let stored = storage.repository(rid).unwrap();
@@ -1259,8 +1197,8 @@ mod tests {
.unwrap();
let signed = stored.sign_refs(&signer).unwrap();
- let remote = stored.remote(&alice).unwrap();
- let mut unsigned = stored.references_of(&alice).unwrap();
+ let remote = stored.remote(alice).unwrap();
+ let mut unsigned = stored.references_of(alice).unwrap();
// The signed refs doesn't contain the signature ref itself.
unsigned.remove_sigrefs().unwrap();
diff --git a/crates/radicle/src/storage/git/cob.rs b/crates/radicle/src/storage/git/cob.rs
index 2ca9eef7c..01f191e8f 100644
--- a/crates/radicle/src/storage/git/cob.rs
+++ b/crates/radicle/src/storage/git/cob.rs
@@ -3,7 +3,6 @@ use std::collections::BTreeMap;
use std::path::Path;
use cob::object::Objects;
-use cob::signatures::ExtendedSignature;
use radicle_cob as cob;
use radicle_cob::change;
use storage::RemoteRepository;
@@ -43,8 +42,6 @@ pub enum TypesError {
#[error(transparent)]
Git(#[from] git::raw::Error),
#[error(transparent)]
- ParseKey(#[from] crypto::Error),
- #[error(transparent)]
ParseObjectId(#[from] cob::object::ParseObjectId),
#[error(transparent)]
RefFormat(#[from] git::fmt::Error),
@@ -58,18 +55,17 @@ impl change::Storage for Repository {
type ObjectId = <git::raw::Repository as change::Storage>::ObjectId;
type Parent = <git::raw::Repository as change::Storage>::Parent;
- type Signatures = <git::raw::Repository as change::Storage>::Signatures;
- fn store<Signer>(
+ type PublicKey = <git::raw::Repository as change::Storage>::PublicKey;
+ type Signature = <git::raw::Repository as change::Storage>::Signature;
+
+ fn store(
&self,
authority: Option<Self::Parent>,
parents: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: change::Template<Self::ObjectId>,
- ) -> Result<cob::Entry, Self::StoreError>
- where
- Signer: crypto::signature::Signer<ExtendedSignature>,
- {
+ ) -> Result<cob::Entry, Self::StoreError> {
self.backend.store(authority, parents, signer, spec)
}
@@ -203,18 +199,17 @@ impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> {
type ObjectId = <git::raw::Repository as change::Storage>::ObjectId;
type Parent = <git::raw::Repository as change::Storage>::Parent;
- type Signatures = <git::raw::Repository as change::Storage>::Signatures;
- fn store<Signer>(
+ type PublicKey = <git::raw::Repository as change::Storage>::PublicKey;
+ type Signature = <git::raw::Repository as change::Storage>::Signature;
+
+ fn store(
&self,
authority: Option<Self::Parent>,
parents: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: change::Template<Self::ObjectId>,
- ) -> Result<cob::Entry, Self::StoreError>
- where
- Signer: crypto::signature::Signer<ExtendedSignature>,
- {
+ ) -> Result<cob::Entry, Self::StoreError> {
self.repo.raw().store(authority, parents, signer, spec)
}
@@ -235,31 +230,21 @@ impl<Repo> SignRepository for DraftStore<'_, Repo>
where
Repo: storage::ReadRepository,
{
- fn sign_refs<Signer>(
+ fn sign_refs(
&self,
- signer: &Signer,
- ) -> Result<storage::refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> Result<storage::refs::SignedRefs, RepositoryError> {
// Since this is a draft store, we do not actually want to sign the refs.
// Instead, we just return the existing signed refs.
- let remote = self.repo.remote(&signer.verifying_key())?;
+ let remote = self.repo.remote(signer.public_key())?;
Ok(remote.refs)
}
- fn force_sign_refs<Signer>(
+ fn force_sign_refs(
&self,
- signer: &Signer,
- ) -> Result<storage::refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> Result<storage::refs::SignedRefs, RepositoryError> {
self.sign_refs(signer)
}
}
diff --git a/crates/radicle/src/storage/refs.rs b/crates/radicle/src/storage/refs.rs
index 639dd5f92..1f4b0a156 100644
--- a/crates/radicle/src/storage/refs.rs
+++ b/crates/radicle/src/storage/refs.rs
@@ -10,8 +10,8 @@ use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::str::FromStr;
-use crypto::signature;
-use crypto::{PublicKey, Signature};
+use crypto::PublicKey;
+use crypto::Signature;
use radicle_core::NodeId;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -70,55 +70,49 @@ impl Refs {
/// Save the signed refs to disk.
/// This creates a new commit on the signed refs branch, and updates the branch pointer.
- pub fn save<R, S>(
+ pub fn save<Repo>(
self,
namespace: NodeId,
committer: sigrefs::git::Committer,
- repo: &R,
- signer: &S,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
) -> Result<SignedRefs, Error>
where
- R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
- R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
- R: HasRepoId,
- S: signature::Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+ Repo: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+ Repo: HasRepoId,
{
self.save_with(namespace, committer, repo, signer, false)
}
/// Save the signed refs to disk, even if the refs are unchanged.
- pub fn force_save<R, S>(
+ pub fn force_save<Repo>(
self,
namespace: NodeId,
committer: sigrefs::git::Committer,
- repo: &R,
- signer: &S,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
) -> Result<SignedRefs, Error>
where
- R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
- R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
- R: HasRepoId,
- S: signature::Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+ Repo: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+ Repo: HasRepoId,
{
self.save_with(namespace, committer, repo, signer, true)
}
- fn save_with<R, S>(
+ fn save_with<Repo>(
self,
namespace: NodeId,
committer: sigrefs::git::Committer,
- repo: &R,
- signer: &S,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
force: bool,
) -> Result<SignedRefs, Error>
where
- R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
- R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
- R: HasRepoId,
- S: signature::Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+ Repo: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+ Repo: HasRepoId,
{
let msg = "Update signed refs\n";
let reflog = format!("Save {} signed references", self.len());
@@ -452,7 +446,12 @@ impl SignedRefs {
R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
{
let root = repo.rid();
- match sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read() {
+
+ let verifier = crypto::VerifyingKey::try_from(&remote).map_err(|source| {
+ sigrefs::read::error::Read::Verify(sigrefs::read::error::Verify::Signature(source))
+ })?;
+
+ match sigrefs::SignedRefsReader::new(root, tip, repo, &verifier).read() {
Ok(latest) => Ok(Some(latest.into_sigrefs_at(remote))),
Err(sigrefs::read::error::Read::MissingSigrefs { namespace }) => {
debug_assert_eq!(namespace, remote);
@@ -500,7 +499,7 @@ mod tests {
use super::*;
use crate::assert_matches;
- use crate::node::device::Device;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::storage::WriteRepository as _;
use crate::{Storage, cob::Title, cob::identity::Identity, rad, test::fixtures};
@@ -522,8 +521,8 @@ mod tests {
// `paris` repo. We also don't expected the signed refs to validate without error.
fn test_rid_verification() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Device::mock();
- let bob = Device::mock();
+ let alice = SigningKey::mock(9);
+ let bob = SigningKey::mock(10);
let storage = &Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
@@ -665,7 +664,7 @@ mod tests {
// under her copy of Bob's namespace, and this would only be rejected during signed ref
// validation.
{
- let name = &SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(bob.node_id()));
+ let name = &SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(bob.public_key()));
let id = paris.backend.refname_to_id(name.as_str()).unwrap();
london
.backend
diff --git a/crates/radicle/src/storage/refs/arbitrary.rs b/crates/radicle/src/storage/refs/arbitrary.rs
index 5cb40075c..67f07d0de 100644
--- a/crates/radicle/src/storage/refs/arbitrary.rs
+++ b/crates/radicle/src/storage/refs/arbitrary.rs
@@ -2,14 +2,9 @@
use qcheck::Arbitrary;
-use crate::node::device::Device;
-
use super::*;
-pub fn signed_refs_at<S>(g: &mut qcheck::Gen, root: Oid, signer: &Device<S>) -> SignedRefs
-where
- S: crypto::signature::Signer<crypto::Signature>,
-{
+pub fn signed_refs_at(g: &mut qcheck::Gen, root: Oid, signer: &impl crypto::Signer) -> SignedRefs {
let mut refs = Refs::arbitrary(g);
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
@@ -25,7 +20,7 @@ where
SignedRefs {
refs,
signature,
- id: *signer.node_id(),
+ id: *signer.public_key(),
level: level.unwrap_or_else(|| FeatureLevel::arbitrary(g)),
parent: Arbitrary::arbitrary(g),
at: Oid::arbitrary(g),
diff --git a/crates/radicle/src/storage/refs/sigrefs/git/properties.rs b/crates/radicle/src/storage/refs/sigrefs/git/properties.rs
index 5c3d636b4..d7ef6c8b6 100644
--- a/crates/radicle/src/storage/refs/sigrefs/git/properties.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/git/properties.rs
@@ -1,7 +1,7 @@
use std::path::Path;
-use crypto::test::signer::MockSigner;
-use crypto::{PublicKey, Signer as _, signature};
+use crypto::{Signer as _, SigningKey};
+use crypto::{signature, signature::Keypair as _};
use qcheck::TestResult;
use qcheck_macros::quickcheck;
use radicle_core::{NodeId, RepoId};
@@ -20,26 +20,6 @@ use super::Committer;
type BoundedVec<T> = arbitrary::BoundedVec<T, 24>;
-struct Verifier {
- key: PublicKey,
-}
-
-impl Verifier {
- fn new(signer: &MockSigner) -> Self {
- Self {
- key: *signer.public_key(),
- }
- }
-}
-
-impl signature::Verifier<crypto::Signature> for Verifier {
- fn verify(&self, msg: &[u8], signature: &crypto::Signature) -> Result<(), signature::Error> {
- self.key
- .verify(msg, signature)
- .map_err(signature::Error::from_source)
- }
-}
-
fn mock_author() -> Author {
Author {
name: "testy".to_string(),
@@ -111,7 +91,7 @@ fn write_log(
refs: Refs,
rid: RepoId,
namespace: NodeId,
- signer: &MockSigner,
+ signer: &SigningKey,
repo: &git::raw::Repository,
) -> Update {
SignedRefsWriter::new(refs, rid, namespace, repo, signer)
@@ -123,12 +103,15 @@ fn write_log(
.unwrap()
}
-fn read_log(
+fn read_log<Verifier>(
rid: RepoId,
namespace: NodeId,
verifier: &Verifier,
repo: &git::raw::Repository,
-) -> VerifiedCommit {
+) -> VerifiedCommit
+where
+ Verifier: signature::Verifier<crypto::Signature>,
+{
SignedRefsReader::new(rid, Tip::Reference(namespace), repo, verifier)
.read()
.unwrap()
@@ -143,9 +126,9 @@ fn initial_commit_roundtrip(mut refs: Refs) -> bool {
_tmp,
} = Repository::new();
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(73);
let namespace = *signer.public_key();
- let verifier = Verifier::new(&signer);
+ let verifier = signer.verifying_key();
let update = write_log(refs.clone(), rid, namespace, &signer, &repo);
let head_oid = match update {
@@ -174,9 +157,9 @@ fn chain_roundtrip(chain: BoundedVec<Refs>) -> TestResult {
root,
_tmp,
} = Repository::new();
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(98);
let namespace = *signer.public_key();
- let verifier = Verifier::new(&signer);
+ let verifier = signer.verifying_key();
let mut last_changed_head = None;
let mut expected_parent = None;
@@ -225,7 +208,7 @@ fn idempotent_write(mut refs: Refs) -> bool {
_tmp,
} = Repository::new();
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(83);
let namespace = *signer.public_key();
let first = write_log(refs.clone(), rid, namespace, &signer, &repo);
diff --git a/crates/radicle/src/storage/refs/sigrefs/property.rs b/crates/radicle/src/storage/refs/sigrefs/property.rs
index 917e7e674..188c43202 100644
--- a/crates/radicle/src/storage/refs/sigrefs/property.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/property.rs
@@ -1,8 +1,8 @@
mod mock;
use mock::*;
-use crypto::Signer as _;
-use crypto::test::signer::MockSigner;
+use crypto::signature::Keypair as _;
+use crypto::{Signer as _, SigningKey};
use qcheck::TestResult;
use qcheck_macros::quickcheck;
@@ -17,8 +17,8 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
}
let fixture = Fixture::new();
- let signer = MockSigner::default();
- let node_id = *signer.public_key();
+ let signer = SigningKey::mock(34);
+ let node_id = signer.public_key();
for refs in all_refs {
let refs = fixture.with_identity_root(refs);
@@ -26,7 +26,7 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
let writer = SignedRefsWriter::new(
refs.clone(),
fixture.rid(),
- node_id,
+ *node_id,
fixture.repo(),
&signer,
);
@@ -46,11 +46,13 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
assert_eq!(refs, written_refs);
+ let verifier = signer.verifying_key();
+
let reader = SignedRefsReader::new(
fixture.rid(),
- Tip::Reference(node_id),
+ Tip::Reference(*node_id),
fixture.repo(),
- &node_id,
+ &verifier,
);
let verified = match reader.read() {
Ok(v) => v,
@@ -69,7 +71,7 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
fn idempotent(refs: Refs) -> TestResult {
let fixture = Fixture::new();
let refs = fixture.with_identity_root(refs);
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(83);
let node_id = *signer.public_key();
if let Err(e) = SignedRefsWriter::new(
diff --git a/crates/radicle/src/storage/refs/sigrefs/read.rs b/crates/radicle/src/storage/refs/sigrefs/read.rs
index a22bd3703..ad3baa805 100644
--- a/crates/radicle/src/storage/refs/sigrefs/read.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/read.rs
@@ -9,7 +9,7 @@ use std::collections::{BTreeMap, HashMap};
use std::num::NonZeroUsize;
use std::path::Path;
-use crypto::{PublicKey, signature};
+use crypto::{PublicKey, VerifyingKey, signature};
use nonempty::NonEmpty;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::commit::CommitData;
@@ -72,11 +72,11 @@ impl VerifiedCommit {
/// - A [`Tip`] which describes where and how to start the verification.
/// - A `repository` which is the Git repository that is being used for the reading.
/// - A `verifier` which is the entity that verifies the cryptographic signatures.
-pub struct SignedRefsReader<'a, R, V> {
+pub struct SignedRefsReader<'a, Repository, Verifier = VerifyingKey> {
rid: RepoId,
tip: Tip,
- repository: &'a R,
- verifier: &'a V,
+ repository: &'a Repository,
+ verifier: &'a Verifier,
}
/// Describe where to start a [`SignedRefsReader`]'s commit chain.
@@ -126,13 +126,13 @@ impl std::fmt::Display for FeatureLevels {
}
}
-impl<'a, R, V> SignedRefsReader<'a, R, V>
+impl<'a, Repository, Verifier> SignedRefsReader<'a, Repository, Verifier>
where
- R: object::Reader + reference::Reader,
- V: signature::Verifier<crypto::Signature>,
+ Repository: object::Reader + reference::Reader,
+ Verifier: signature::Verifier<crypto::Signature>,
{
/// Construct a new [`SignedRefsReader`].
- pub fn new(rid: RepoId, tip: Tip, repository: &'a R, verifier: &'a V) -> Self {
+ pub fn new(rid: RepoId, tip: Tip, repository: &'a Repository, verifier: &'a Verifier) -> Self {
Self {
rid,
tip,
@@ -191,6 +191,19 @@ where
return Ok(head);
}
+ /// A wrapper around a [`crypto::Signature`] that implements
+ /// [`std::hash::Hash`] so that we can use it as a key in a [`HashMap`].
+ #[derive(PartialEq, Eq)]
+ #[repr(transparent)]
+ struct HashSignature(crypto::Signature);
+
+ impl std::hash::Hash for HashSignature {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ state.write(self.0.r_bytes());
+ state.write(self.0.s_bytes());
+ }
+ }
+
// `seen` maps from signatures to the `NonEmpty` of commits they were
// seen in. Note that for all sets of commits which share the same
// signature, the `NonEmpty` in `seen` will be in reverse order of the
@@ -201,13 +214,13 @@ where
// the maximum feature level over all commits in the history.
let (seen, levels) = iter::Walk::new(head.commit.oid, self.repository).try_fold(
(
- HashMap::<crypto::Signature, NonEmpty<Oid>>::new(),
+ HashMap::<HashSignature, NonEmpty<Oid>>::new(),
FeatureLevels::new(),
),
|(mut seen, mut levels), commit| {
let commit = commit.map_err(error::Read::Commit)?;
- seen.entry(commit.signature)
+ seen.entry(HashSignature(commit.signature))
.and_modify(|value| value.push(commit.oid))
.or_insert_with(|| NonEmpty::new(commit.oid));
@@ -242,7 +255,7 @@ where
}
if seen
- .get(&head.commit.signature)
+ .get(&HashSignature(head.commit.signature))
.expect(SIGNATURES_COLLECTED)
.len_nonzero()
== ONE
@@ -272,7 +285,9 @@ where
let commit = verified.commit();
- let commits = seen.get(&commit.signature).expect(SIGNATURES_COLLECTED);
+ let commits = seen
+ .get(&HashSignature(commit.signature))
+ .expect(SIGNATURES_COLLECTED);
if commits.len_nonzero() == ONE {
return Ok(verified);
}
@@ -345,13 +360,13 @@ impl Commit {
&self.signature
}
- pub(super) fn verify<V>(
+ pub(super) fn verify<Verifier>(
mut self,
expected: RepoId,
- verifier: &V,
+ verifier: &Verifier,
) -> Result<VerifiedCommit, error::Verify>
where
- V: signature::Verifier<crypto::Signature>,
+ Verifier: signature::Verifier<crypto::Signature>,
{
verifier
.verify(&self.refs.canonical(), &self.signature)
@@ -433,16 +448,13 @@ impl Commit {
}
}
-pub(super) struct CommitReader<'a, R> {
+pub(super) struct CommitReader<'a, Repository: object::Reader> {
commit: Oid,
- repository: &'a R,
+ repository: &'a Repository,
}
-impl<'a, R> CommitReader<'a, R>
-where
- R: object::Reader,
-{
- pub(super) fn new(commit: Oid, repository: &'a R) -> Self {
+impl<'a, Repo: object::Reader> CommitReader<'a, Repo> {
+ pub(super) fn new(commit: Oid, repository: &'a Repo) -> Self {
Self { commit, repository }
}
@@ -502,16 +514,13 @@ struct Tree {
signature: crypto::Signature,
}
-struct TreeReader<'a, R> {
+struct TreeReader<'a, Repo: object::Reader> {
commit: Oid,
- repository: &'a R,
+ repository: &'a Repo,
}
-impl<'a, R> TreeReader<'a, R>
-where
- R: object::Reader,
-{
- fn new(commit: Oid, repository: &'a R) -> Self {
+impl<'a, Repo: object::Reader> TreeReader<'a, Repo> {
+ fn new(commit: Oid, repository: &'a Repo) -> Self {
Self { commit, repository }
}
@@ -566,16 +575,13 @@ pub(super) struct IdentityRoot {
rid: RepoId,
}
-struct IdentityRootReader<'a, 'b, R> {
+struct IdentityRootReader<'a, 'b, Repo: object::Reader> {
refs: &'a Refs,
- repository: &'b R,
+ repository: &'b Repo,
}
-impl<'a, 'b, R> IdentityRootReader<'a, 'b, R>
-where
- R: object::Reader,
-{
- fn new(refs: &'a Refs, repository: &'b R) -> Self {
+impl<'a, 'b, Repo: object::Reader> IdentityRootReader<'a, 'b, Repo> {
+ fn new(refs: &'a Refs, repository: &'b Repo) -> Self {
Self { refs, repository }
}
diff --git a/crates/radicle/src/storage/refs/sigrefs/read/error.rs b/crates/radicle/src/storage/refs/sigrefs/read/error.rs
index c7a8c0626..dfb66520b 100644
--- a/crates/radicle/src/storage/refs/sigrefs/read/error.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/read/error.rs
@@ -81,7 +81,7 @@ pub enum Tree {
#[error(transparent)]
ParseRefs(canonical::Error),
#[error(transparent)]
- ParseSignature(crypto::Error),
+ ParseSignature(crypto::signature::Error),
#[error(transparent)]
MissingBlobs(#[from] MissingBlobs),
}
diff --git a/crates/radicle/src/storage/refs/sigrefs/write.rs b/crates/radicle/src/storage/refs/sigrefs/write.rs
index a427fb7fb..674f5c0c3 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write.rs
@@ -6,7 +6,7 @@ mod test;
use std::path::Path;
use crypto::PublicKey;
-use crypto::signature::{self, Signer};
+use crypto::signature;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::author::Author;
use radicle_git_metadata::commit::{CommitData, headers::Headers, trailers::OwnedTrailer};
@@ -62,19 +62,17 @@ impl Update {
/// writing.
/// - A `signer` which is the entity that produces the cryptographic signature
/// over the [`Refs`].
-pub struct SignedRefsWriter<'a, R, S> {
+pub struct SignedRefsWriter<'a, Repo, Signer: crypto::Signer> {
refs: Refs,
rid: RepoId,
namespace: NodeId,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
}
-impl<'a, R, S> SignedRefsWriter<'a, R, S>
+impl<'a, Repo, Signer: crypto::Signer> SignedRefsWriter<'a, Repo, Signer>
where
- R: object::Writer + object::Reader + reference::Writer + reference::Reader,
- S: Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: object::Writer + object::Reader + reference::Writer + reference::Reader,
{
/// Construct a new [`SignedRefsWriter`].
///
@@ -87,8 +85,8 @@ where
mut refs: Refs,
rid: RepoId,
namespace: NodeId,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
) -> Self {
debug_assert!(refs.get(&IDENTITY_ROOT).is_some());
debug_assert!(refs.get(&SIGREFS_PARENT).is_none());
@@ -157,7 +155,8 @@ where
} = self;
let reference = SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&namespace));
- let head = HeadReader::new(&reference, repository, rid, self.signer).read();
+ let verifier = signer.verifying_key();
+ let head = HeadReader::new(&reference, repository, rid, &verifier).read();
let commit_writer = match head {
Ok(Some(head)) if !force && head.is_unchanged(&refs) => {
@@ -221,21 +220,23 @@ impl Commit {
}
}
-struct CommitWriter<'a, R, S> {
+struct CommitWriter<'a, Repo, Signer: crypto::Signer> {
refs: Refs,
parent: Option<Oid>,
author: Author,
message: String,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
}
-impl<'a, R, S> CommitWriter<'a, R, S>
-where
- R: object::Writer,
- S: Signer<crypto::Signature>,
-{
- fn root(refs: Refs, author: Author, message: String, repository: &'a R, signer: &'a S) -> Self {
+impl<'a, Repo: object::Writer, Signer: crypto::Signer> CommitWriter<'a, Repo, Signer> {
+ fn root(
+ refs: Refs,
+ author: Author,
+ message: String,
+ repository: &'a Repo,
+ signer: &'a Signer,
+ ) -> Self {
Self {
refs,
parent: None,
@@ -251,8 +252,8 @@ where
parent: Oid,
author: Author,
message: String,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
) -> Self {
Self {
refs,
@@ -307,18 +308,14 @@ struct Tree {
signature: crypto::Signature,
}
-struct TreeWriter<'a, R, S> {
+struct TreeWriter<'a, Repo, Signer: crypto::Signer> {
refs: Refs,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
}
-impl<'a, R, S> TreeWriter<'a, R, S>
-where
- R: object::Writer,
- S: Signer<crypto::Signature>,
-{
- fn new(refs: Refs, repository: &'a R, signer: &'a S) -> Self {
+impl<'a, Repo: object::Writer, Signer: crypto::Signer> TreeWriter<'a, Repo, Signer> {
+ fn new(refs: Refs, repository: &'a Repo, signer: &'a Signer) -> Self {
Self {
refs,
repository,
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs
index 8d82731d7..3d0582270 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs
@@ -24,7 +24,7 @@ fn tree_error() {
mock::author(),
"msg".into(),
&repo,
- &mock::NeverSign,
+ &mock::NeverSign::new(),
)
.write();
assert!(matches!(result, Err(error::Commit::Tree(_))));
@@ -40,7 +40,7 @@ fn write_commit_error() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write();
assert!(matches!(result, Err(error::Commit::Write(_))));
@@ -58,7 +58,7 @@ fn write_root_ok() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write()
.unwrap();
@@ -82,7 +82,7 @@ fn write_with_parent_ok() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write()
.unwrap();
@@ -105,7 +105,7 @@ fn write_empty_refs() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write()
.unwrap();
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs
index 448c21988..4736438fb 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs
@@ -9,7 +9,7 @@ use crate::storage::refs::{IDENTITY_ROOT, Refs, SIGREFS_BRANCH};
fn read(repo: &MockRepository) -> Result<Option<Head>, error::Head> {
let namespace = mock::node_id();
let reference = SIGREFS_BRANCH.with_namespace(Component::from(&namespace));
- HeadReader::new(&reference, repo, mock::rid(), &mock::AlwaysSign).read()
+ HeadReader::new(&reference, repo, mock::rid(), &mock::AlwaysSign::new()).read()
}
fn refs() -> [(crate::git::fmt::RefString, radicle_oid::Oid); 2] {
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
index 23ff266a4..a7bfc2a99 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
@@ -1,6 +1,5 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
-use std::str::FromStr as _;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::author::{Author, Time};
@@ -132,6 +131,14 @@ impl MockRepository {
self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), vec![sig_id; 64])
}
+ pub fn with_valid_signature(self, commit: Oid, refs: &Refs) -> MockRepository {
+ let signer = signing_key();
+ let signature = crypto::signature::Signer::try_sign(&signer, &refs.canonical())
+ .expect("mock signing key must sign refs");
+
+ self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), signature.to_vec())
+ }
+
pub fn with_signature_error(self, commit: Oid) -> MockRepository {
self.with_blob_error(commit, Path::new(SIGNATURE_BLOB_PATH))
}
@@ -142,7 +149,6 @@ impl MockRepository {
pub fn with_invalid_signature(self, commit: Oid) -> MockRepository {
let bytes = vec![0u8; 1];
- assert!(crypto::Signature::from_str(std::str::from_utf8(&bytes).unwrap()).is_err());
self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), bytes)
}
@@ -285,12 +291,21 @@ impl reference::Writer for MockRepository {
}
}
+fn signing_key() -> crypto::SigningKey {
+ crypto::SigningKey::mock(1)
+}
+
/// Always signs successfully, returning a fixed 64-byte signature.
-pub struct AlwaysSign;
+#[derive(Clone)]
+pub struct AlwaysSign(crypto::SigningKey);
impl AlwaysSign {
const SIGNATURE: [u8; 64] = [1u8; 64];
+ pub fn new() -> Self {
+ AlwaysSign(signing_key())
+ }
+
pub fn signature() -> crypto::Signature {
crypto::Signature::from(Self::SIGNATURE)
}
@@ -312,8 +327,36 @@ impl crypto::signature::Verifier<crypto::Signature> for AlwaysSign {
}
}
+impl crypto::signature::Keypair for AlwaysSign {
+ type VerifyingKey = crypto::VerifyingKey;
+
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ self.0.verifying_key()
+ }
+}
+
+impl AsRef<crypto::PublicKey> for AlwaysSign {
+ fn as_ref(&self) -> &crypto::PublicKey {
+ self.0.as_ref()
+ }
+}
+
/// Always fails to sign.
-pub struct NeverSign;
+pub struct NeverSign(crypto::SigningKey);
+
+impl NeverSign {
+ pub fn new() -> Self {
+ NeverSign(signing_key())
+ }
+}
+
+impl crypto::signature::Keypair for NeverSign {
+ type VerifyingKey = crypto::VerifyingKey;
+
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ self.0.verifying_key()
+ }
+}
impl crypto::signature::Signer<crypto::Signature> for NeverSign {
fn try_sign(&self, _msg: &[u8]) -> Result<crypto::Signature, crypto::signature::Error> {
@@ -321,6 +364,12 @@ impl crypto::signature::Signer<crypto::Signature> for NeverSign {
}
}
+impl AsRef<crypto::PublicKey> for NeverSign {
+ fn as_ref(&self) -> &crypto::PublicKey {
+ self.0.as_ref()
+ }
+}
+
/// Construct an [`Oid`] from a single repeated byte.
///
/// `oid(1) != oid(2)` is guaranteed; use distinct values for distinct objects.
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs
index d58015a08..2d20abffb 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs
@@ -39,7 +39,14 @@ fn refs_with_rad_sigrefs() -> Refs {
}
fn write(refs: Refs, repo: &MockRepository) -> Result<Update, error::Write> {
- SignedRefsWriter::new(refs, mock::rid(), mock::node_id(), repo, &mock::AlwaysSign).write(
+ SignedRefsWriter::new(
+ refs,
+ mock::rid(),
+ mock::node_id(),
+ repo,
+ &mock::AlwaysSign::new(),
+ )
+ .write(
Committer::new(mock::author()),
"msg".into(),
"reflog".into(),
@@ -47,7 +54,14 @@ fn write(refs: Refs, repo: &MockRepository) -> Result<Update, error::Write> {
}
fn force_write(refs: Refs, repo: &MockRepository) -> Result<Update, error::Write> {
- SignedRefsWriter::new(refs, mock::rid(), mock::node_id(), repo, &mock::AlwaysSign).force_write(
+ SignedRefsWriter::new(
+ refs,
+ mock::rid(),
+ mock::node_id(),
+ repo,
+ &mock::AlwaysSign::new(),
+ )
+ .force_write(
Committer::new(mock::author()),
"msg".into(),
"reflog".into(),
@@ -71,7 +85,7 @@ fn unchanged() {
.with_rad_sigrefs(&mock::node_id(), head)
.with_commit(head, mock::commit_data([]))
.with_refs(head, refs.clone())
- .with_signature(head, 1);
+ .with_valid_signature(head, &refs);
match write(refs.clone(), &repo).unwrap() {
Update::Unchanged { verified } => {
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs
index 91fd2b2a5..06e8dbddc 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs
@@ -11,14 +11,15 @@ fn mock_refs() -> Refs {
fn sign_error() {
// `NeverSign` fails before `write_tree` is ever called, so the
// repository needs no configuration.
- let result = TreeWriter::new(mock_refs(), &MockRepository::new(), &mock::NeverSign).write();
+ let result =
+ TreeWriter::new(mock_refs(), &MockRepository::new(), &mock::NeverSign::new()).write();
assert!(matches!(result, Err(error::Tree::Sign(_))));
}
#[test]
fn write_tree_error() {
let repo = MockRepository::new().with_write_tree_error();
- let result = TreeWriter::new(mock_refs(), &repo, &mock::AlwaysSign).write();
+ let result = TreeWriter::new(mock_refs(), &repo, &mock::AlwaysSign::new()).write();
assert!(matches!(result, Err(error::Tree::Write(_))));
}
@@ -27,7 +28,7 @@ fn write_ok() {
let refs = mock_refs();
let expected_oid = mock::oid(1);
let repo = MockRepository::new().with_write_tree_ok(expected_oid);
- let tree = TreeWriter::new(refs.clone(), &repo, &mock::AlwaysSign)
+ let tree = TreeWriter::new(refs.clone(), &repo, &mock::AlwaysSign::new())
.write()
.unwrap();
assert_eq!(
diff --git a/crates/radicle/src/test.rs b/crates/radicle/src/test.rs
index d21c3d95d..c47136226 100644
--- a/crates/radicle/src/test.rs
+++ b/crates/radicle/src/test.rs
@@ -75,8 +75,7 @@ pub mod setup {
use tempfile::{TempDir, tempdir};
use super::storage::{Namespaces, RefUpdate};
- use crate::crypto::test::signer::MockSigner;
- use crate::node::device::Device;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::storage::git::Repository;
use crate::storage::git::transport::remote;
use crate::{Storage, git, profile::Home, rad::REMOTE_NAME, test::fixtures};
@@ -89,20 +88,20 @@ pub mod setup {
pub tmp: TempDir,
pub root: PathBuf,
pub storage: Storage,
- pub signer: Device<MockSigner>,
+ pub signer: SigningKey,
}
impl Default for Node {
fn default() -> Self {
let root = tempdir().unwrap();
- Self::new(root, MockSigner::default(), "Radcliff")
+ Self::new(root, SigningKey::mock(73), "Radcliff")
}
}
impl Node {
- pub fn new(tmp: TempDir, signer: MockSigner, alias: &str) -> Self {
- let signer = Device::from(signer);
+ pub fn new(tmp: TempDir, signer: SigningKey, alias: &str) -> Self {
+ let signer = signer;
let root = tmp.path().to_path_buf();
let home = root.join("home");
let paths = Home::new(home.as_path()).unwrap();
@@ -251,10 +250,10 @@ pub mod setup {
impl Default for Network {
fn default() -> Self {
- let alice = Node::new(tempdir().unwrap(), MockSigner::from_seed([!0; 32]), "alice");
- let mut bob = Node::new(tempdir().unwrap(), MockSigner::from_seed([!1; 32]), "bob");
- let mut eve = Node::new(tempdir().unwrap(), MockSigner::from_seed([!2; 32]), "eve");
- let mut dave = Node::new(tempdir().unwrap(), MockSigner::from_seed([!3; 32]), "dave");
+ let alice = Node::new(tempdir().unwrap(), SigningKey::mock(!0), "alice");
+ let mut bob = Node::new(tempdir().unwrap(), SigningKey::mock(!1), "bob");
+ let mut eve = Node::new(tempdir().unwrap(), SigningKey::mock(!2), "eve");
+ let mut dave = Node::new(tempdir().unwrap(), SigningKey::mock(!3), "dave");
let repo = alice.project();
let rid = repo.id;
diff --git a/crates/radicle/src/test/arbitrary.rs b/crates/radicle/src/test/arbitrary.rs
index 903e1d43c..20b959fe8 100644
--- a/crates/radicle/src/test/arbitrary.rs
+++ b/crates/radicle/src/test/arbitrary.rs
@@ -65,6 +65,10 @@ pub fn vec<T: Eq + Arbitrary>(size: usize) -> Vec<T> {
vec
}
+pub fn vec_distinct<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> Vec<T> {
+ set(range).into_iter().collect::<Vec<_>>()
+}
+
pub fn nonempty_storage(size: usize) -> MockStorage {
let mut storage = r#gen::<MockStorage>(size);
for _ in 0..size {
@@ -166,11 +170,8 @@ impl Arbitrary for RawDoc {
impl Arbitrary for Doc {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let mut rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let project = Project::arbitrary(g);
- let delegates = iter::repeat_with(|| Did::arbitrary(g))
- .take(rng.usize(1..6))
- .collect::<Vec<_>>();
+ let delegates = vec_distinct::<Did>(1..6);
let threshold = delegates.len() / 2 + 1;
let visibility = Visibility::arbitrary(g);
let doc = RawDoc::new(project, delegates, threshold, visibility);
@@ -243,7 +244,7 @@ impl Arbitrary for Address {
AddressType::Onion => {
let pk = PublicKey::arbitrary(g);
let addr = OnionAddrV3::from(
- cyphernet::ed25519::PublicKey::from_pk_compressed(pk.to_byte_array().into())
+ cyphernet::ed25519::PublicKey::from_pk_compressed(pk.into_inner().into())
.unwrap(),
);
cyphernet::addr::HostName::Tor(addr)
diff --git a/crates/radicle/src/test/fixtures.rs b/crates/radicle/src/test/fixtures.rs
index 496c96b6f..c2578c5f3 100644
--- a/crates/radicle/src/test/fixtures.rs
+++ b/crates/radicle/src/test/fixtures.rs
@@ -6,7 +6,6 @@ use crate::git;
use crate::identity::RepoId;
use crate::identity::doc::Visibility;
use crate::node::Alias;
-use crate::node::device::Device;
use crate::rad;
use crate::storage::git::Storage;
use crate::storage::git::transport;
@@ -30,22 +29,24 @@ pub fn user() -> git::UserInfo {
}
/// Create a new storage with a project.
-pub fn storage<P, G>(path: P, signer: &Device<G>) -> Result<Storage, rad::InitError>
-where
- P: AsRef<Path>,
- G: crypto::signature::Signer<crypto::Signature>,
-{
+pub fn storage(
+ path: impl AsRef<Path>,
+ signer: &impl crypto::Signer,
+) -> Result<Storage, rad::InitError> {
let path = path.as_ref();
+
+ let key = signer.public_key();
+
let storage = Storage::open(
path.join("storage"),
git::UserInfo {
alias: Alias::new("Radcliff"),
- key: *signer.public_key(),
+ key: *key,
},
)?;
transport::local::register(storage.clone());
- transport::remote::mock::register(signer.public_key(), storage.path());
+ transport::remote::mock::register(key, storage.path());
for (name, desc) in [
("acme", "Acme's repository"),
@@ -68,15 +69,11 @@ where
}
/// Create a new repository at the given path, and initialize it into a project.
-pub fn project<P, G>(
- path: P,
+pub fn project(
+ path: impl AsRef<Path>,
storage: &Storage,
- signer: &Device<G>,
-) -> Result<(RepoId, SignedRefs, git::raw::Repository, git::raw::Oid), rad::InitError>
-where
- P: AsRef<Path>,
- G: crypto::signature::Signer<crypto::Signature>,
-{
+ signer: &impl crypto::Signer,
+) -> Result<(RepoId, SignedRefs, git::raw::Repository, git::raw::Oid), rad::InitError> {
transport::local::register(storage.clone());
let (working, head) = repository(path);
diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs
index fb5ee4978..7755828d1 100644
--- a/crates/radicle/src/test/storage.rs
+++ b/crates/radicle/src/test/storage.rs
@@ -6,6 +6,7 @@ use std::str::FromStr;
use crypto::PublicKey;
+use crate::crypto::ExtendedSignature;
pub use crate::git;
use crate::git::fmt;
@@ -379,23 +380,17 @@ impl WriteRepository for MockRepository {
}
impl SignRepository for MockRepository {
- fn sign_refs<Signer>(
+ fn sign_refs(
&self,
- _signer: &Signer,
- ) -> Result<crate::storage::refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- {
+ _signer: &impl crypto::Signer,
+ ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> {
todo!()
}
- fn force_sign_refs<Signer>(&self, _signer: &Signer) -> Result<refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ fn force_sign_refs(
+ &self,
+ _signer: &impl crypto::Signer,
+ ) -> Result<refs::SignedRefs, RepositoryError> {
todo!()
}
}
@@ -453,21 +448,24 @@ impl radicle_cob::change::Storage for MockRepository {
type LoadError = radicle_cob::git::change::error::Load;
type ObjectId = Oid;
type Parent = Oid;
- type Signatures = radicle_cob::signatures::ExtendedSignature;
- fn store<G>(
+ type PublicKey = crypto::PublicKey;
+ type Signature = crypto::Signature;
+
+ fn store(
&self,
_resource: Option<Self::Parent>,
_related: Vec<Self::Parent>,
- _signer: &G,
+ _signer: &impl crypto::Signer,
_template: radicle_cob::change::Template<Self::ObjectId>,
) -> Result<
- radicle_cob::change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>,
+ radicle_cob::change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
Self::StoreError,
- >
- where
- G: radicle_crypto::signature::Signer<Self::Signatures>,
- {
+ > {
todo!()
}
@@ -475,7 +473,11 @@ impl radicle_cob::change::Storage for MockRepository {
&self,
_id: Self::ObjectId,
) -> Result<
- radicle_cob::change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>,
+ radicle_cob::change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
Self::LoadError,
> {
todo!()
commit c81ffc0ec14c5c25400025e26542f3b33f3e5685
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Mon Jun 29 14:06:31 2026 +0200
radicle, cli/auth: Let binary crate generate seed
In preparation of a rewrite of `radicle-crypto`, move the
responsibility for generating a cryptographically secure
seed from `radicle` (transitively via dependency on
`radicle-crypto` and `ec25519`) to the dependent of `radicle`.
Applications should decide how to obtain randomness, not `radicle` via
some far-removed dependency.
Within the workspace, the only dependent using `Profile::seed` is
`radicle-cli`, in particular for the `rad auth` subcommand. To obtain
random bytes, `radicle-cli` depends on `getrandom` and uses
`getrandom::fill`.
diff --git a/Cargo.lock b/Cargo.lock
index 2e7448e7c..f2e73a09c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1134,12 +1134,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -1199,15 +1193,13 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
- "wasip2",
- "wasip3",
]
[[package]]
@@ -1445,7 +1437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b"
dependencies = [
"gix-hash",
- "hashbrown 0.16.1",
+ "hashbrown",
"parking_lot",
]
@@ -1805,15 +1797,6 @@ dependencies = [
"byteorder",
]
-[[package]]
-name = "hashbrown"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
-dependencies = [
- "foldhash",
-]
-
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1972,12 +1955,6 @@ dependencies = [
"zerovec",
]
-[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
[[package]]
name = "idna"
version = "1.1.0"
@@ -2006,7 +1983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
- "hashbrown 0.16.1",
+ "hashbrown",
"serde",
"serde_core",
]
@@ -2174,12 +2151,6 @@ dependencies = [
"spin",
]
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
[[package]]
name = "lexopt"
version = "0.3.2"
@@ -2779,16 +2750,6 @@ dependencies = [
"yansi",
]
-[[package]]
-name = "prettyplease"
-version = "0.2.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
-dependencies = [
- "proc-macro2",
- "syn 2.0.117",
-]
-
[[package]]
name = "primeorder"
version = "0.13.6"
@@ -2953,6 +2914,7 @@ dependencies = [
"clap",
"clap_complete",
"dunce",
+ "getrandom 0.4.3",
"human-panic",
"humantime",
"itertools",
@@ -4234,7 +4196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.4.2",
+ "getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -4651,12 +4613,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
-[[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
[[package]]
name = "unit-prefix"
version = "0.5.2"
@@ -4703,7 +4659,7 @@ version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
"js-sys",
"wasm-bindgen",
]
@@ -4813,15 +4769,6 @@ dependencies = [
"wit-bindgen",
]
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen",
-]
-
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
@@ -4867,40 +4814,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
-]
-
[[package]]
name = "web-time"
version = "1.1.0"
@@ -5320,88 +5233,6 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck",
- "indexmap",
- "prettyplease",
- "syn 2.0.117",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
[[package]]
name = "writeable"
diff --git a/crates/radicle-cli/Cargo.toml b/crates/radicle-cli/Cargo.toml
index afe6f91b8..89896ade1 100644
--- a/crates/radicle-cli/Cargo.toml
+++ b/crates/radicle-cli/Cargo.toml
@@ -24,6 +24,7 @@ chrono = { workspace = true, features = ["clock", "std"] }
clap = { version = "4.5.44", features = ["derive"] }
clap_complete = "4.5"
dunce = { workspace = true }
+getrandom = "0.4.3"
human-panic.workspace = true
humantime.workspace = true
itertools.workspace = true
diff --git a/crates/radicle-cli/src/commands/auth.rs b/crates/radicle-cli/src/commands/auth.rs
index cd5a49915..ca9374228 100644
--- a/crates/radicle-cli/src/commands/auth.rs
+++ b/crates/radicle-cli/src/commands/auth.rs
@@ -56,7 +56,18 @@ pub fn init(args: Args) -> anyhow::Result<()> {
};
let passphrase = passphrase.filter(|passphrase| !passphrase.trim().is_empty());
let spinner = term::spinner("Creating your Ed25519 keypair…");
- let profile = Profile::init(home, alias, passphrase.clone(), env::seed())?;
+ let profile = Profile::init(
+ home,
+ alias,
+ passphrase.clone(),
+ env::seed().unwrap_or_else(|| {
+ use radicle::crypto::Seed;
+
+ let mut seed = [0; Seed::BYTES];
+ getrandom::fill(&mut seed).expect("failed get random bytes from the operating system");
+ Seed::new(seed)
+ }),
+ )?;
let mut agent = true;
spinner.finish();
diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs
index b70867e8d..403bd5706 100644
--- a/crates/radicle/src/profile.rs
+++ b/crates/radicle/src/profile.rs
@@ -39,6 +39,8 @@ use crate::{cob, git, node, storage};
pub mod env {
pub use std::env::*;
+ use crypto::Seed;
+
/// Path to the Radicle home folder.
pub const RAD_HOME: &str = "RAD_HOME";
/// Path to the Radicle node socket file.
@@ -129,22 +131,31 @@ pub mod env {
}
/// Return the seed stored in the [`RAD_KEYGEN_SEED`] environment variable,
- /// or generate a random one.
- pub fn seed() -> crypto::Seed {
- if let Ok(seed) = var(RAD_KEYGEN_SEED) {
- let Ok(seed) = (0..seed.len())
- .step_by(2)
- .map(|i| u8::from_str_radix(&seed[i..i + 2], 16))
- .collect::<Result<Vec<u8>, _>>()
- else {
- panic!("env::seed: invalid hexadecimal value set in `{RAD_KEYGEN_SEED}`");
- };
- let Ok(seed): Result<[u8; 32], _> = seed.try_into() else {
- panic!("env::seed: invalid seed length set in `{RAD_KEYGEN_SEED}`");
- };
- crypto::Seed::new(seed)
- } else {
- crypto::Seed::generate()
+ /// if set. Otherwise, return `None`.
+ ///
+ /// # Panics
+ ///
+ /// If the environment value [`RAD_KEYGEN_SEED`] is set but malformed,
+ /// i.e., not Unicode, not hexadecimal, or not 32 bytes long.
+ pub fn seed() -> Option<Seed> {
+ match var(RAD_KEYGEN_SEED) {
+ Err(VarError::NotPresent) => None,
+ Err(VarError::NotUnicode(_)) => {
+ panic!("env::seed: invalid Unicode value set in `{RAD_KEYGEN_SEED}`")
+ }
+ Ok(seed) => Some(Seed::new(
+ (0..seed.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&seed[i..i + 2], 16))
+ .collect::<Result<Vec<_>, _>>()
+ .unwrap_or_else(|_| {
+ panic!("env::seed: invalid hexadecimal value set in `{RAD_KEYGEN_SEED}`")
+ })
+ .try_into()
+ .unwrap_or_else(|_| {
+ panic!("env::seed: invalid seed length set in `{RAD_KEYGEN_SEED}`")
+ }),
+ )),
}
}
commit 9cfaa2bd7d14855213f60bb0e9a2f008892a0a5a
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Fri Jul 3 22:41:25 2026 +0200
crypto/test: Diffie-Hellman Fixture
Before rewriting `radicle-crypto`, add a fixture to have some assurance
on compatibility.
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index a8d3ec4ba..21769df0a 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -544,4 +544,31 @@ mod tests {
assert!(!hm.insert(a));
assert!(!hm.insert(b));
}
+
+ #[test]
+ fn e25519_dh_fixture() {
+ let sk_a: [u8; 32] = [
+ 92, 136, 18, 88, 112, 205, 201, 68, 109, 197, 130, 211, 179, 138, 197, 113, 120, 55,
+ 104, 139, 208, 184, 178, 157, 120, 11, 60, 13, 91, 30, 213, 38,
+ ];
+ let sk_b: [u8; 32] = [
+ 202, 152, 225, 201, 169, 81, 217, 16, 235, 104, 91, 252, 52, 113, 81, 190, 68, 250, 86,
+ 21, 202, 228, 123, 193, 140, 252, 63, 72, 5, 137, 36, 245,
+ ];
+
+ let kp_a = KeyPair::from_seed(ec25519::Seed::from(sk_a));
+ let kp_b = KeyPair::from_seed(ec25519::Seed::from(sk_b));
+
+ let output_a = SecretKey::from(kp_b.sk).ecdh(&kp_a.pk.into()).unwrap();
+ let output_b = SecretKey::from(kp_a.sk).ecdh(&kp_b.pk.into()).unwrap();
+
+ assert_eq!(output_a, output_b);
+ assert_eq!(
+ output_a,
+ [
+ 159, 131, 169, 27, 132, 202, 47, 250, 112, 247, 176, 222, 213, 220, 147, 216, 53,
+ 7, 33, 232, 232, 77, 254, 105, 125, 237, 61, 243, 209, 172, 93, 100
+ ]
+ )
+ }
}
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name d0edeb0a-e1bd-41d1-bdaa-bf310d599c16 -v /opt/radcis/ci.rad.levitte.org/cci/state/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/s:/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w:/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w -w /d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.97.1-x86_64-unknown-linux-gnu'
info: latest update on 2026-07-16, rust version 1.97.1 (8bab26f4f 2026-07-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.97.1 (c980f4866 2026-06-30)
+ rustc --version
rustc 1.97.1 (8bab26f4f 2026-07-14)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- --deny warnings
Updating crates.io index
Downloading crates ...
Downloaded borrow-or-share v0.2.4
Downloaded blowfish v0.9.1
Downloaded bytecount v0.6.9
Downloaded fastrand v2.3.0
Downloaded bytesize v2.3.1
Downloaded chacha20 v0.9.1
Downloaded chacha20poly1305 v0.10.1
Downloaded addr2line v0.25.1
Downloaded chrono v0.4.44
Downloaded byteorder v1.5.0
Downloaded cbc v0.1.2
Downloaded cfg-if v1.0.4
Downloaded bytes v1.11.1
Downloaded cc v1.2.57
Downloaded bstr v1.12.1
Downloaded potential_utf v0.1.4
Downloaded proc-macro-error-attr3 v3.0.2
Downloaded gix-hash v0.25.0
Downloaded pkcs8 v0.10.2
Downloaded ppv-lite86 v0.2.21
Downloaded pkcs1 v0.7.5
Downloaded pin-project-lite v0.2.17
Downloaded pkg-config v0.3.32
Downloaded polyval v0.6.2
Downloaded primeorder v0.13.6
Downloaded structured-logger v1.0.5
Downloaded git-ref-format-core v0.6.0
Downloaded poly1305 v0.8.0
Downloaded proc-macro-error3 v3.0.2
Downloaded referencing v0.30.0
Downloaded pretty_assertions v1.4.1
Downloaded convert_case v0.10.0
Downloaded portable-atomic v1.13.1
Downloaded getrandom v0.3.4
Downloaded curve25519-dalek-derive v0.1.1
Downloaded fancy-regex v0.14.0
Downloaded const-str v0.4.3
Downloaded cyphernet v0.5.4
Downloaded clap_derive v4.6.0
Downloaded idna v1.1.0
Downloaded data-encoding-macro v0.1.19
Downloaded gix-command v0.9.0
Downloaded getrandom v0.4.3
Downloaded jsonschema v0.30.0
Downloaded ed25519-dalek v2.2.0
Downloaded equivalent v1.0.2
Downloaded git-ref-format v0.7.0
Downloaded gix-commitgraph v0.37.0
Downloaded form_urlencoded v1.2.2
Downloaded getrandom v0.2.17
Downloaded strsim v0.11.1
Downloaded colorchoice v1.0.5
Downloaded ecdsa v0.16.9
Downloaded generic-array v0.14.7
Downloaded clap_complete v4.6.0
Downloaded data-encoding v2.10.0
Downloaded emojis v0.6.4
Downloaded shell-words v1.1.1
Downloaded signal-hook-registry v1.4.8
Downloaded signature v2.2.0
Downloaded clap_lex v1.1.0
Downloaded console v0.16.3
Downloaded git-ref-format-macro v0.7.0
Downloaded gix-error v0.2.3
Downloaded signature v1.6.4
Downloaded signal-hook v0.3.18
Downloaded diff v0.1.13
Downloaded gix-actor v0.41.0
Downloaded anstyle-query v1.1.5
Downloaded crypto-bigint v0.5.5
Downloaded data-encoding-macro-internal v0.1.17
Downloaded gix-credentials v0.38.0
Downloaded clap v4.6.0
Downloaded scrypt v0.11.0
Downloaded socks5-client v0.4.3
Downloaded qcheck v1.0.0
Downloaded serde_derive_internals v0.29.1
Downloaded signal-hook-mio v0.2.5
Downloaded crypto-common v0.1.7
Downloaded either v1.15.0
Downloaded fnv v1.0.7
Downloaded gix-glob v0.26.0
Downloaded rand_xorshift v0.4.0
Downloaded snapbox-macros v1.1.0
Downloaded faster-hex v0.10.0
Downloaded streaming-iterator v0.1.9
Downloaded dunce v1.0.5
Downloaded erased-serde v0.4.10
Downloaded ref-cast-impl v1.0.25
Downloaded ssh-cipher v0.2.0
Downloaded stable_deref_trait v1.2.1
Downloaded cyphergraphy v0.3.1
Downloaded sha2 v0.10.9
Downloaded crc32fast v1.5.0
Downloaded ctr v0.9.2
Downloaded env_logger v0.11.9
Downloaded crossbeam-channel v0.5.15
Downloaded cipher v0.4.4
Downloaded gix-fs v0.21.1
Downloaded ref-cast v1.0.25
Downloaded subtle v2.6.1
Downloaded serde_spanned v1.0.4
Downloaded spin v0.9.8
Downloaded gix-diff v0.63.0
Downloaded escargot v0.5.15
Downloaded dyn-clone v1.0.20
Downloaded cypheraddr v0.4.1
Downloaded fast-glob v0.3.3
Downloaded derive_more v2.1.1
Downloaded ghash v0.5.1
Downloaded gix-chunk v0.7.1
Downloaded errno v0.3.14
Downloaded sqlite3-sys v0.18.0
Downloaded same-file v1.0.6
Downloaded flate2 v1.1.9
Downloaded siphasher v0.3.11
Downloaded base-x v0.2.11
Downloaded cpufeatures v0.2.17
Downloaded document-features v0.2.12
Downloaded email_address v0.2.9
Downloaded gix-hashtable v0.15.0
Downloaded crossbeam-utils v0.8.21
Downloaded gix-sec v0.14.0
Downloaded ec25519 v0.1.0
Downloaded autocfg v1.5.0
Downloaded ed25519 v2.2.3
Downloaded env_filter v1.0.0
Downloaded gix-url v0.36.0
Downloaded digest v0.10.7
Downloaded ed25519 v1.5.3
Downloaded find-msvc-tools v0.1.9
Downloaded sval_fmt v2.17.0
Downloaded gix-prompt v0.15.0
Downloaded colored v2.2.0
Downloaded gix-traverse v0.57.0
Downloaded gix-refspec v0.41.0
Downloaded rfc6979 v0.4.0
Downloaded serde_fmt v1.1.0
Downloaded fluent-uri v0.3.2
Downloaded synstructure v0.13.2
Downloaded ff v0.13.1
Downloaded quick-error v1.2.3
Downloaded elliptic-curve v0.13.8
Downloaded const-oid v0.9.6
Downloaded der v0.7.10
Downloaded bitflags v2.11.0
Downloaded tinystr v0.8.2
Downloaded gix-features v0.48.0
Downloaded crossterm v0.29.0
Downloaded iana-time-zone v0.1.65
Downloaded fraction v0.15.3
Downloaded gix-object v0.60.0
Downloaded git2 v0.21.0
Downloaded derive_more-impl v2.1.1
Downloaded gix-config-value v0.18.0
Downloaded icu_normalizer_data v2.1.1
Downloaded rand_core v0.6.4
Downloaded scopeguard v1.2.0
Downloaded filetime v0.2.27
Downloaded tree-sitter-highlight v0.24.7
Downloaded displaydoc v0.2.5
Downloaded inout v0.1.4
Downloaded sha1 v0.10.6
Downloaded gix-packetline v0.21.3
Downloaded match-lookup v0.1.2
Downloaded radicle-git-ext v0.13.0
Downloaded idna_adapter v1.2.1
Downloaded is_terminal_polyfill v1.70.2
Downloaded clap_builder v4.6.0
Downloaded test-log v0.2.19
Downloaded gix-date v0.15.3
Downloaded salsa20 v0.10.2
Downloaded radicle-std-ext v0.2.0
Downloaded secrecy v0.10.3
Downloaded writeable v0.6.2
Downloaded hmac v0.12.1
Downloaded walkdir v2.5.0
Downloaded rand_chacha v0.3.1
Downloaded siphasher v1.0.2
Downloaded qcheck-macros v1.0.0
Downloaded ssh-encoding v0.2.0
Downloaded tree-sitter-json v0.24.8
Downloaded test-log-macros v0.2.19
Downloaded tree-sitter-html v0.23.2
Downloaded sval_nested v2.17.0
Downloaded ahash v0.8.12
Downloaded opaque-debug v0.3.1
Downloaded nonempty v0.9.0
Downloaded keccak v0.1.6
Downloaded sval_ref v2.17.0
Downloaded matchers v0.2.0
Downloaded unicode-normalization v0.1.25
Downloaded litrs v1.0.0
Downloaded log v0.4.29
Downloaded num-integer v0.1.46
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded hashbrown v0.16.1
Downloaded rustc_version v0.4.1
Downloaded gix-validate v0.11.1
Downloaded rustc-demangle v0.1.27
Downloaded sqlite v0.37.0
Downloaded anstyle-parse v0.2.7
Downloaded unicode-ident v1.0.24
Downloaded zerovec-derive v0.11.2
Downloaded quote v1.0.45
Downloaded gix-negotiate v0.31.0
Downloaded zerofrom-derive v0.1.6
Downloaded multibase v0.9.2
Downloaded toml v0.9.12+spec-1.1.0
Downloaded ssh-agent-lib v0.6.0
Downloaded backtrace v0.3.76
Downloaded sval_dynamic v2.17.0
Downloaded sval_json v2.17.0
Downloaded uuid-simd v0.8.0
Downloaded num-iter v0.1.45
Downloaded version_check v0.9.5
Downloaded unarray v0.1.4
Downloaded nonempty v0.12.0
Downloaded gix-quote v0.7.1
Downloaded zerofrom v0.1.6
Downloaded uuid v1.22.0
Downloaded unicode-segmentation v1.12.0
Downloaded rustversion v1.0.22
Downloaded simd-adler32 v0.3.8
Downloaded spki v0.7.3
Downloaded zerotrie v0.2.3
Downloaded systemd-journal-logger v2.2.2
Downloaded curve25519-dalek v4.1.3
Downloaded amplify_derive v4.0.1
Downloaded anstream v0.6.21
Downloaded semver v1.0.27
Downloaded sem_safe v0.2.1
Downloaded anstyle-parse v1.0.0
Downloaded rand_chacha v0.9.0
Downloaded shlex v1.3.0
Downloaded rusty-fork v0.3.1
Downloaded sec1 v0.7.3
Downloaded sharded-slab v0.1.7
Downloaded gimli v0.32.3
Downloaded lexopt v0.3.2
Downloaded inquire v0.9.4
Downloaded value-bag-sval2 v1.12.0
Downloaded normalize-line-endings v0.3.0
Downloaded block-padding v0.3.3
Downloaded gix-trace v0.1.19
Downloaded group v0.13.0
Downloaded phf v0.11.3
Downloaded universal-hash v0.5.1
Downloaded itoa v1.0.17
Downloaded value-bag-serde1 v1.12.0
Downloaded gix-odb v0.80.0
Downloaded thiserror-impl v1.0.69
Downloaded serde_core v1.0.228
Downloaded base16ct v0.2.0
Downloaded wait-timeout v0.2.1
Downloaded zerovec v0.11.5
Downloaded lazy_static v1.5.0
Downloaded typeid v1.0.3
Downloaded pastey v0.2.1
Downloaded base256emoji v1.0.2
Downloaded hash32 v0.3.1
Downloaded anstream v1.0.0
Downloaded human-panic v2.0.6
Downloaded maybe-async v0.2.10
Downloaded num-rational v0.4.2
Downloaded thiserror v1.0.69
Downloaded tinyvec_macros v0.1.1
Downloaded gix-utils v0.3.2
Downloaded percent-encoding v2.3.2
Downloaded bcrypt-pbkdf v0.10.0
Downloaded phf_shared v0.11.3
Downloaded vsimd v0.8.0
Downloaded amplify_num v0.5.3
Downloaded icu_properties_data v2.1.2
Downloaded icu_provider v2.1.1
Downloaded yoke-derive v0.8.1
Downloaded prodash v31.0.0
Downloaded bit-vec v0.8.0
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded block-buffer v0.10.4
Downloaded serde-untagged v0.1.9
Downloaded sval_buffer v2.17.0
Downloaded gix-path v0.12.0
Downloaded utf8parse v0.2.2
Downloaded signals_receipts v0.2.5
Downloaded smallvec v1.15.1
Downloaded nu-ansi-term v0.50.3
Downloaded zeroize_derive v1.5.0
Downloaded gix-revwalk v0.31.0
Downloaded tempfile v3.27.0
Downloaded once_cell v1.21.4
Downloaded memmap2 v0.9.10
Downloaded itertools v0.14.0
Downloaded tree-sitter-css v0.23.2
Downloaded tinyvec v1.11.0
Downloaded gix-pack v0.70.0
Downloaded gix-shallow v0.12.0
Downloaded pbkdf2 v0.12.2
Downloaded zeroize v1.9.0
Downloaded proc-macro2 v1.0.106
Downloaded serde_derive v1.0.228
Downloaded snapbox v1.2.2
Downloaded num-cmp v0.1.0
Downloaded pem-rfc7468 v0.7.0
Downloaded utf8_iter v1.0.4
Downloaded tree-sitter-go v0.23.4
Downloaded jobserver v0.1.34
Downloaded amplify_syn v2.0.1
Downloaded toml_writer v1.0.7+spec-1.1.0
Downloaded xattr v1.6.1
Downloaded base64ct v1.8.3
Downloaded bit-set v0.8.0
Downloaded heck v0.5.0
Downloaded parking_lot v0.12.5
Downloaded num-complex v0.4.6
Downloaded rand_core v0.9.5
Downloaded lock_api v0.4.14
Downloaded noise-framework v0.4.1
Downloaded unicode-display-width v0.3.0
Downloaded gix-lock v23.0.0
Downloaded aead v0.5.2
Downloaded unit-prefix v0.5.2
Downloaded adler2 v2.0.1
Downloaded timeago v0.4.2
Downloaded sval_serde v2.17.0
Downloaded thiserror v2.0.18
Downloaded ryu v1.0.23
Downloaded zmij v1.0.21
Downloaded socket2 v0.5.10
Downloaded schemars_derive v1.2.1
Downloaded yoke v0.8.1
Downloaded icu_locale_core v2.1.1
Downloaded base32 v0.4.0
Downloaded tracing-log v0.2.0
Downloaded num v0.4.3
Downloaded rsa v0.9.10
Downloaded anyhow v1.0.102
Downloaded humantime v2.3.0
Downloaded num-traits v0.2.19
Downloaded tree-sitter-language v0.1.7
Downloaded p256 v0.13.2
Downloaded value-bag v1.12.0
Downloaded thread_local v1.1.9
Downloaded jiff-static v0.2.23
Downloaded anstyle v1.0.14
Downloaded litemap v0.8.1
Downloaded gix-ref v0.63.0
Downloaded gix-protocol v0.61.0
Downloaded gix-tempfile v23.0.0
Downloaded tar v0.4.45
Downloaded icu_normalizer v2.1.1
Downloaded amplify v4.9.0
Downloaded ascii v1.1.0
Downloaded outref v0.5.2
Downloaded rand v0.8.5
Downloaded icu_collections v2.1.1
Downloaded gix-revision v0.45.0
Downloaded parking_lot_core v0.9.12
Downloaded gix-transport v0.57.0
Downloaded thiserror-impl v2.0.18
Downloaded aho-corasick v1.1.4
Downloaded base64 v0.22.1
Downloaded base64 v0.21.7
Downloaded miniz_oxide v0.8.9
Downloaded icu_properties v2.1.2
Downloaded serde v1.0.228
Downloaded indicatif v0.18.4
Downloaded yansi v1.0.1
Downloaded similar v3.1.1
Downloaded schemars v1.2.1
Downloaded sval v2.17.0
Downloaded heapless v0.8.0
Downloaded arc-swap v1.9.1
Downloaded url v2.5.8
Downloaded p521 v0.13.3
Downloaded ssh-key v0.6.7
Downloaded rand v0.9.2
Downloaded indexmap v2.13.0
Downloaded memchr v2.8.0
Downloaded num-bigint-dig v0.8.6
Downloaded num-bigint v0.4.6
Downloaded aes-gcm v0.10.3
Downloaded tracing-core v0.1.36
Downloaded tree-sitter-python v0.23.6
Downloaded x25519-dalek v2.0.1
Downloaded unicode-width v0.2.2
Downloaded regex v1.12.3
Downloaded typenum v1.20.1
Downloaded vcpkg v0.2.15
Downloaded zlib-rs v0.6.5
Downloaded libm v0.2.16
Downloaded zerocopy v0.8.42
Downloaded serde_json v1.0.149
Downloaded syn v2.0.117
Downloaded tree-sitter-c v0.23.4
Downloaded radicle-surf v0.28.0
Downloaded tree-sitter-rust v0.23.3
Downloaded mio v1.1.1
Downloaded tracing v0.1.44
Downloaded aes v0.8.4
Downloaded tree-sitter-bash v0.23.3
Downloaded tracing-subscriber v0.3.23
Downloaded tree-sitter v0.24.7
Downloaded proptest v1.10.0
Downloaded p384 v0.13.1
Downloaded libc v0.2.183
Downloaded jiff v0.2.23
Downloaded tokio v1.50.0
Downloaded libz-sys v1.1.25
Downloaded sysinfo v0.37.2
Downloaded syn v1.0.109
Downloaded tree-sitter-md v0.3.2
Downloaded object v0.37.3
Downloaded bloomy v1.2.0
Downloaded regex-syntax v0.8.10
Downloaded rustix v1.1.4
Downloaded tree-sitter-ruby v0.23.1
Downloaded regex-automata v0.4.14
Downloaded sha3 v0.10.8
Downloaded tree-sitter-typescript v0.23.2
Downloaded sha1-checked v0.10.0
Downloaded libgit2-sys v0.18.4+1.9.3
Downloaded sqlite3-src v0.7.0
Downloaded linux-raw-sys v0.12.1
Compiling libc v0.2.183
Compiling proc-macro2 v1.0.106
Compiling quote v1.0.45
Compiling unicode-ident v1.0.24
Compiling serde_core v1.0.228
Checking cfg-if v1.0.4
Compiling serde v1.0.228
Compiling version_check v0.9.5
Compiling generic-array v0.14.7
Compiling syn v2.0.117
Checking getrandom v0.2.17
Checking typenum v1.20.1
Checking rand_core v0.6.4
Checking memchr v2.8.0
Compiling find-msvc-tools v0.1.9
Compiling jobserver v0.1.34
Compiling shlex v1.3.0
Checking subtle v2.6.1
Compiling cc v1.2.57
Checking const-oid v0.9.6
Checking regex-syntax v0.8.10
Checking aho-corasick v1.1.4
Checking smallvec v1.15.1
Checking cpufeatures v0.2.17
Compiling thiserror v2.0.18
Checking regex-automata v0.4.14
Compiling parking_lot_core v0.9.12
Checking fastrand v2.3.0
Checking scopeguard v1.2.0
Checking lock_api v0.4.14
Checking stable_deref_trait v1.2.1
Checking parking_lot v0.12.5
Checking bitflags v2.11.0
Compiling typeid v1.0.3
Compiling erased-serde v0.4.10
Checking itoa v1.0.17
Compiling crc32fast v1.5.0
Checking gix-trace v0.1.19
Checking tinyvec_macros v0.1.1
Checking tinyvec v1.11.0
Checking serde_fmt v1.1.0
Checking hashbrown v0.16.1
Checking unicode-normalization v0.1.25
Checking value-bag-serde1 v1.12.0
Checking byteorder v1.5.0
Checking gix-utils v0.3.2
Checking value-bag v1.12.0
Checking same-file v1.0.6
Checking walkdir v2.5.0
Compiling serde_derive v1.0.228
Compiling zeroize_derive v1.5.0
Compiling thiserror-impl v2.0.18
Checking bstr v1.12.1
Checking log v0.4.29
Checking gix-validate v0.11.1
Checking prodash v31.0.0
Compiling autocfg v1.5.0
Compiling libm v0.2.16
Checking zlib-rs v0.6.5
Compiling num-traits v0.2.19
Checking gix-path v0.12.0
Compiling rustix v1.1.4
Compiling heapless v0.8.0
Compiling pkg-config v0.3.32
Checking hash32 v0.3.1
Checking gix-features v0.48.0
Checking linux-raw-sys v0.12.1
Compiling getrandom v0.4.3
Checking faster-hex v0.10.0
Compiling zerocopy v0.8.42
Checking zeroize v1.9.0
Compiling zmij v1.0.21
Checking equivalent v1.0.2
Compiling serde_json v1.0.149
Checking once_cell v1.21.4
Checking indexmap v2.13.0
Checking crypto-common v0.1.7
Checking block-buffer v0.10.4
Checking digest v0.10.7
Checking block-padding v0.3.3
Checking inout v0.1.4
Checking sha2 v0.10.9
Checking sha1 v0.10.6
Checking sha1-checked v0.10.0
Checking cipher v0.4.4
Checking gix-hash v0.25.0
Compiling ref-cast v1.0.25
Checking der v0.7.10
Compiling ref-cast-impl v1.0.25
Checking spin v0.9.8
Checking lazy_static v1.5.0
Checking dyn-clone v1.0.20
Compiling syn v1.0.109
Compiling vcpkg v0.2.15
Compiling thiserror v1.0.69
Checking tempfile v3.27.0
Compiling thiserror-impl v1.0.69
Compiling tree-sitter-language v0.1.7
Compiling libz-sys v1.1.25
Checking ppv-lite86 v0.2.21
Checking hmac v0.12.1
Checking universal-hash v0.5.1
Checking num-integer v0.1.46
Compiling serde_derive_internals v0.29.1
Checking opaque-debug v0.3.1
Checking spki v0.7.3
Checking signature v2.2.0
Compiling libgit2-sys v0.18.4+1.9.3
Checking ff v0.13.1
Checking base16ct v0.2.0
Checking sec1 v0.7.3
Checking group v0.13.0
Compiling schemars_derive v1.2.1
Checking rand_chacha v0.3.1
Checking crypto-bigint v0.5.5
Compiling data-encoding v2.10.0
Checking schemars v1.2.1
Compiling semver v1.0.27
Compiling data-encoding-macro-internal v0.1.17
Compiling rustc_version v0.4.1
Checking elliptic-curve v0.13.8
Compiling amplify_syn v2.0.1
Checking rand v0.8.5
Checking num-iter v0.1.45
Checking aead v0.5.2
Compiling match-lookup v0.1.2
Checking signature v1.6.4
Checking const-str v0.4.3
Checking data-encoding-macro v0.1.19
Compiling amplify_derive v4.0.1
Checking ed25519 v1.5.3
Compiling curve25519-dalek v4.1.3
Checking base256emoji v1.0.2
Checking poly1305 v0.8.0
Checking rfc6979 v0.4.0
Checking chacha20 v0.9.1
Checking gix-error v0.2.3
Checking amplify_num v0.5.3
Checking ascii v1.1.0
Checking base-x v0.2.11
Checking multibase v0.9.2
Checking ecdsa v0.16.9
Checking ec25519 v0.1.0
Checking git-ref-format-core v0.6.0
Checking amplify v4.9.0
Checking primeorder v0.13.6
Checking polyval v0.6.2
Compiling curve25519-dalek-derive v0.1.1
Compiling num-bigint-dig v0.8.6
Checking base64ct v1.8.3
Checking ghash v0.5.1
Checking cyphergraphy v0.3.1
Checking pem-rfc7468 v0.7.0
Checking pkcs8 v0.10.2
Checking pbkdf2 v0.12.2
Checking ctr v0.9.2
Checking aes v0.8.4
Compiling sqlite3-src v0.7.0
Checking keccak v0.1.6
Checking aes-gcm v0.10.3
Checking sha3 v0.10.8
Checking pkcs1 v0.7.5
Checking ssh-encoding v0.2.0
Checking ed25519 v2.2.3
Checking blowfish v0.9.1
Checking cbc v0.1.2
Checking base32 v0.4.0
Checking cypheraddr v0.4.1
Checking rsa v0.9.10
Checking ssh-cipher v0.2.0
Checking bcrypt-pbkdf v0.10.0
Checking ed25519-dalek v2.2.0
Checking p521 v0.13.3
Checking p384 v0.13.1
Checking p256 v0.13.2
Checking chacha20poly1305 v0.10.1
Checking qcheck v1.0.0
Checking ssh-key v0.6.7
Checking noise-framework v0.4.1
Checking socks5-client v0.4.3
Checking secrecy v0.10.3
Compiling synstructure v0.13.2
Checking percent-encoding v2.3.2
Checking ssh-agent-lib v0.6.0
Checking cyphernet v0.5.4
Checking x25519-dalek v2.0.1
Checking utf8parse v0.2.2
Checking jiff v0.2.23
Checking anstyle-query v1.1.5
Compiling zerofrom-derive v0.1.6
Checking zerofrom v0.1.6
Compiling yoke-derive v0.8.1
Checking radicle-localtime v0.1.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-localtime)
Checking errno v0.3.14
Checking is_terminal_polyfill v1.70.2
Checking anstyle v1.0.14
Checking colorchoice v1.0.5
Checking yoke v0.8.1
Checking gix-hashtable v0.15.0
Checking gix-date v0.15.3
Checking nonempty v0.9.0
Checking siphasher v1.0.2
Checking gix-actor v0.41.0
Checking gix-object v0.60.0
Checking radicle-git-metadata v0.2.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-git-metadata)
Compiling zerovec-derive v0.11.2
Checking radicle-dag v0.10.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-dag)
Checking memmap2 v0.9.10
Checking nonempty v0.12.0
Checking radicle-git-ref-format v0.1.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-git-ref-format)
Checking gix-chunk v0.7.1
Checking zerovec v0.11.5
Compiling displaydoc v0.2.5
Compiling radicle v0.24.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle)
Checking base64 v0.21.7
Checking gix-commitgraph v0.37.0
Checking anstyle-parse v1.0.0
Checking serde-untagged v0.1.9
Checking bytesize v2.3.1
Checking dunce v1.0.5
Checking fast-glob v0.3.3
Checking gix-revwalk v0.31.0
Checking anstream v1.0.0
Checking gix-fs v0.21.1
Checking mio v1.1.1
Checking sem_safe v0.2.1
Checking signals_receipts v0.2.5
Checking gix-tempfile v23.0.0
Checking tinystr v0.8.2
Checking gix-quote v0.7.1
Checking writeable v0.6.2
Checking litemap v0.8.1
Checking zerotrie v0.2.3
Checking icu_locale_core v2.1.1
Checking potential_utf v0.1.4
Compiling unicode-segmentation v1.12.0
Compiling icu_properties_data v2.1.2
Compiling icu_normalizer_data v2.1.1
Checking iana-time-zone v0.1.65
Compiling signal-hook v0.3.18
Checking shell-words v1.1.1
Checking either v1.15.0
Checking gix-command v0.9.0
Checking chrono v0.4.44
Checking icu_provider v2.1.1
Compiling convert_case v0.10.0
Checking icu_collections v2.1.1
Checking radicle-signals v0.11.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-signals)
Checking signal-hook-registry v1.4.8
Checking colored v2.2.0
Compiling rustversion v1.0.22
Compiling object v0.37.3
Compiling derive_more-impl v2.1.1
Checking gix-lock v23.0.0
Checking gix-url v0.36.0
Checking gix-config-value v0.18.0
Checking gix-sec v0.14.0
Compiling litrs v1.0.0
Checking gimli v0.32.3
Checking unicode-width v0.2.2
Checking adler2 v2.0.1
Compiling portable-atomic v1.13.1
Checking miniz_oxide v0.8.9
Compiling document-features v0.2.12
Checking addr2line v0.25.1
Checking gix-prompt v0.15.0
Checking derive_more v2.1.1
Checking icu_normalizer v2.1.1
Checking icu_properties v2.1.2
Checking signal-hook-mio v0.2.5
Checking radicle-log v0.1.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-log)
Checking gix-revision v0.45.0
Checking gix-traverse v0.57.0
Checking gix-diff v0.63.0
Checking anstyle-parse v0.2.7
Checking gix-packetline v0.21.3
Checking gix-glob v0.26.0
Compiling anyhow v1.0.102
Checking rustc-demangle v0.1.27
Checking backtrace v0.3.76
Checking gix-refspec v0.41.0
Checking gix-transport v0.57.0
Checking anstream v0.6.21
Checking gix-pack v0.70.0
Checking arc-swap v1.9.1
Checking idna_adapter v1.2.1
Checking crossterm v0.29.0
Checking gix-credentials v0.38.0
Checking console v0.16.3
Checking gix-ref v0.63.0
Checking gix-shallow v0.12.0
Checking gix-negotiate v0.31.0
Checking regex v1.12.3
Compiling maybe-async v0.2.10
Compiling tree-sitter v0.24.7
Compiling proc-macro-error-attr3 v3.0.2
Checking utf8_iter v1.0.4
Checking unit-prefix v0.5.2
Compiling getrandom v0.3.4
Compiling simd-adler32 v0.3.8
Checking indicatif v0.18.4
Checking idna v1.1.0
Checking gix-protocol v0.61.0
Compiling proc-macro-error3 v3.0.2
Checking inquire v0.9.4
Checking gix-odb v0.80.0
Compiling xattr v1.6.1
Checking unicode-display-width v0.3.0
Compiling filetime v0.2.27
Checking uuid v1.22.0
Checking bytes v1.11.1
Compiling flate2 v1.1.9
Compiling tar v0.4.45
Compiling git-ref-format-macro v0.7.0
Checking snapbox-macros v1.1.0
Checking salsa20 v0.10.2
Checking siphasher v0.3.11
Compiling crossbeam-utils v0.8.21
Compiling heck v0.5.0
Checking normalize-line-endings v0.3.0
Checking strsim v0.11.1
Checking streaming-iterator v0.1.9
Checking clap_lex v1.1.0
Checking similar v3.1.1
Checking clap_builder v4.6.0
Checking snapbox v1.2.2
Compiling clap_derive v4.6.0
Checking bloomy v1.2.0
Compiling radicle-surf v0.28.0
Checking scrypt v0.11.0
Checking git-ref-format v0.7.0
Checking sqlite3-sys v0.18.0
Checking form_urlencoded v1.2.2
Checking sqlite v0.37.0
Checking systemd-journal-logger v2.2.2
Compiling tree-sitter-css v0.23.2
Checking radicle-crypto v0.17.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-crypto)
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-python v0.23.6
Checking toml_datetime v0.7.5+spec-1.1.0
Checking serde_spanned v1.0.4
Checking pin-project-lite v0.2.17
Checking toml_writer v1.0.7+spec-1.1.0
Checking radicle-std-ext v0.2.0
Checking tokio v1.50.0
Checking toml v0.9.12+spec-1.1.0
Checking clap v4.6.0
Checking url v2.5.8
Checking sysinfo v0.37.2
Checking diff v0.1.13
Compiling radicle-node v0.20.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-node)
Compiling radicle-cli v0.21.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-cli)
Checking yansi v1.0.1
Checking human-panic v2.0.6
Checking pretty_assertions v1.4.1
Checking crossbeam-channel v0.5.15
Checking clap_complete v4.6.0
Checking structured-logger v1.0.5
Checking radicle-systemd v0.13.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.7
Checking itertools v0.14.0
Checking socket2 v0.5.10
Checking timeago v0.4.2
Checking humantime v2.3.0
Compiling escargot v0.5.15
Checking lexopt v0.3.2
Checking bit-vec v0.8.0
Checking bit-set v0.8.0
Checking rand_core v0.9.5
Compiling qcheck-macros v1.0.0
Checking num-bigint v0.4.6
Compiling ahash v0.8.12
Checking num-complex v0.4.6
Checking env_filter v1.0.0
Checking borrow-or-share v0.2.4
Checking fluent-uri v0.3.2
Checking env_logger v0.11.9
Checking phf_shared v0.11.3
Compiling test-log-macros v0.2.19
Checking wait-timeout v0.2.1
Checking num-rational v0.4.2
Checking quick-error v1.2.3
Checking vsimd v0.8.0
Checking fnv v1.0.7
Checking num v0.4.3
Compiling radicle-remote-helper v0.17.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-remote-helper)
Checking outref v0.5.2
Checking fraction v0.15.3
Checking uuid-simd v0.8.0
Checking rusty-fork v0.3.1
Checking test-log v0.2.19
Checking phf v0.11.3
Checking referencing v0.30.0
Checking rand_chacha v0.9.0
Checking rand v0.9.2
Checking rand_xorshift v0.4.0
Checking fancy-regex v0.14.0
Checking email_address v0.2.9
Checking bytecount v0.6.9
Checking base64 v0.22.1
Checking num-cmp v0.1.0
Checking unarray v0.1.4
Checking proptest v1.10.0
Checking emojis v0.6.4
Checking jsonschema v0.30.0
Compiling pastey v0.2.1
Checking radicle-windows v0.1.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-windows)
Checking git2 v0.21.0
Checking radicle-oid v0.2.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-oid)
Checking radicle-term v0.18.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-term)
Checking radicle-git-ext v0.13.0
Checking radicle-cob v0.20.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-cob)
Checking radicle-core v0.3.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-core)
Checking radicle-cli-test v0.13.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-cli-test)
Checking radicle-fetch v0.20.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-fetch)
Checking radicle-schemars v0.8.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-schemars)
Checking radicle-protocol v0.8.0 (/d0edeb0a-e1bd-41d1-bdaa-bf310d599c16/w/crates/radicle-protocol)
error[E0599]: no associated function or constant named `BYTES` found for struct `radicle::radicle_crypto::Seed` in the current scope
--> crates/radicle-cli/src/commands/auth.rs:65:38
|
65 | let mut seed = [0; Seed::BYTES];
| ^^^^^ associated function or constant not found in `radicle::radicle_crypto::Seed`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `radicle-cli` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
Exit code: 101
{
"response": "finished",
"result": "failure"
}