rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodff4817213ba216a8d0876f4d6740e76a19002de0
{
"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": "Updated",
"patch": {
"id": "24e3c53626bd5e1d4a71c188e8d897f47eddbbcb",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"title": "crypto: Rewrite",
"state": {
"status": "open",
"conflicts": []
},
"before": "66ab7f35c933278b932657936ce0c52a02962302",
"after": "ff4817213ba216a8d0876f4d6740e76a19002de0",
"commits": [
"ff4817213ba216a8d0876f4d6740e76a19002de0",
"9edcb11a1d050b1b6f8e11c265d1359d382870b3",
"d18c9f69c66b6cf6d0092b0e9c65ff5202a614a8",
"0fd0104e8771fa7f2bec64216b3668485ff6b324",
"8ceef3c40571809910e4c207f3e55e95f8834490"
],
"target": "66ab7f35c933278b932657936ce0c52a02962302",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "24e3c53626bd5e1d4a71c188e8d897f47eddbbcb",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"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
},
{
"id": "f0c57fd16cfb88f1ca13c46a9ec930b6efb238aa",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"description": "Optimize compute-heavy dependencies",
"base": "027de89cdb93f9f08e46ab76ab38c62c4005724a",
"oid": "6ce96910bbb6d33857ce04afe27d17491b15ed7d",
"timestamp": 1784289364
},
{
"id": "ed140d9a6f2a111ea19054aa78bbebc9b5bfd15f",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"description": "REVIEW\n\nA lot of these are nits and clarifications.\n\nHowever, there does seem to be a major problem. When I tried to push a I got test failures in `radicle`:\n\n```\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::sync::announce::test::announcer_reached_min_replication_target ... FAILED\n\n failures:\n\n failures:\n node::sync::announce::test::announcer_reached_min_replication_target\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 399 filtered out; finished in 0.06s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::sync::announce::test::announcer_reached_min_replication_target' (1087202) panicked at crates/radicle/src/node/sync/announce.rs:684:9:\n assertion `left == right` failed\n left: {PublicKey([5, 221, 106, 2, 242, 199, 33, 65, 145, 133, 231, 152, 124, 221, 98, 121, 221, 220, 3, 227, 226, 20, 123, 218, 187, 218, 83, 133, 33, 178, 59, 103]), PublicKey([23, 166, 132, 119, 154, 93, 182, 134, 242, 16, 90, 181, 129, 140, 75, 214, 100, 111, 77, 40, 166, 193, 195, 32, 155, 14, 238, 118, 41, 84, 233, 71]), PublicKey([75, 248, 102, 239, 154, 3, 237, 239, 125, 238, 107, 133, 162, 159, 138, 180, 2, 58, 59, 67, 223, 26, 98, 15, 82, 15, 176, 22, 72, 74, 130, 219]), PublicKey([96, 17, 171, 105, 226, 198, 100, 220, 161, 223, 167, 106, 196, 89, 213, 84, 128, 207, 1, 237, 232, 174, 197, 173, 56, 217, 189, 40, 10, 83, 230, 103]), PublicKey([108, 193, 114, 1, 252, 37, 158, 110, 74, 55, 0, 248, 215, 176, 195, 164, 101, 48, 247, 239, 141, 7, 239, 106, 255, 58, 174, 144, 207, 23, 190, 13]), PublicKey([157, 2, 236, 151, 9, 210, 225, 195, 187, 105, 75, 49, 11, 211, 109, 140, 194, 77, 241, 2, 97, 68, 95, 248, 96, 184, 137, 206, 174, 217, 7, 169]), PublicKey([183, 102, 88, 18, 108, 214, 199, 242, 120, 129, 243, 76, 196, 98, 239, 193, 168, 66, 79, 199, 175, 59, 132, 99, 22, 63, 190, 64, 100, 85, 210, 190]), PublicKey([189, 232, 81, 145, 168, 63, 139, 84, 87, 160, 247, 28, 67, 156, 16, 213, 28, 204, 121, 177, 75, 153, 128, 50, 145, 112, 5, 106, 22, 3, 193, 72])}\n right: {PublicKey([5, 221, 106, 2, 242, 199, 33, 65, 145, 133, 231, 152, 124, 221, 98, 121, 221, 220, 3, 227, 226, 20, 123, 218, 187, 218, 83, 133, 33, 178, 59, 103]), PublicKey([23, 166, 132, 119, 154, 93, 182, 134, 242, 16, 90, 181, 129, 140, 75, 214, 100, 111, 77, 40, 166, 193, 195, 32, 155, 14, 238, 118, 41, 84, 233, 71]), PublicKey([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]), PublicKey([75, 248, 102, 239, 154, 3, 237, 239, 125, 238, 107, 133, 162, 159, 138, 180, 2, 58, 59, 67, 223, 26, 98, 15, 82, 15, 176, 22, 72, 74, 130, 219]), PublicKey([96, 17, 171, 105, 226, 198, 100, 220, 161, 223, 167, 106, 196, 89, 213, 84, 128, 207, 1, 237, 232, 174, 197, 173, 56, 217, 189, 40, 10, 83, 230, 103]), PublicKey([108, 193, 114, 1, 252, 37, 158, 110, 74, 55, 0, 248, 215, 176, 195, 164, 101, 48, 247, 239, 141, 7, 239, 106, 255, 58, 174, 144, 207, 23, 190, 13]), PublicKey([157, 2, 236, 151, 9, 210, 225, 195, 187, 105, 75, 49, 11, 211, 109, 140, 194, 77, 241, 2, 97, 68, 95, 248, 96, 184, 137, 206, 174, 217, 7, 169]), PublicKey([183, 102, 88, 18, 108, 214, 199, 242, 120, 129, 243, 76, 196, 98, 239, 193, 168, 66, 79, 199, 175, 59, 132, 99, 22, 63, 190, 64, 100, 85, 210, 190]), PublicKey([189, 232, 81, 145, 168, 63, 139, 84, 87, 160, 247, 28, 67, 156, 16, 213, 28, 204, 121, 177, 75, 153, 128, 50, 145, 112, 5, 106, 22, 3, 193, 72])}\n\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds ... FAILED\n\n failures:\n\n failures:\n node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 399 filtered out; finished in 0.04s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds' (1087212) panicked at crates/radicle/src/node/sync/announce.rs:1000:21:\n assertion `left == right` failed: Should succeed with preferred seeds even with zero replication factor\n left: PreferredNodes { preferred: 1, total_nodes_synced: 1 }\n right: PreferredNodes { preferred: 2, total_nodes_synced: 2 }\n```\n\nWhen I reran just the `radicle` tests, I got flakey behaviour with two other tests failing randomly:\n\n```\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::policy::store::test::test_node_aliases ... FAILED\n\n failures:\n\n failures:\n node::policy::store::test::test_node_aliases\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 396 filtered out; finished in 0.06s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::policy::store::test::test_node_aliases' (1115523) panicked at crates/radicle/src/node.rs:1553:9:\n assertion `left == right` failed\n left: Some({PublicKey([12, 112, 75, 77, 19, 96, 38, 134, 98, 141, 230, 13, 191, 16, 6, 101, 198, 62, 95, 184, 134, 54, 54, 163, 177, 47, 254, 215, 192, 216, 85, 24]), PublicKey([34, 60, 233, 140, 45, 106, 150, 104, 75, 232, 227, 240, 199, 87, 107, 242, 128, 46, 157, 26, 159, 100, 54, 74, 113, 201, 232, 242, 96, 237, 160, 39])})\n right: Some({PublicKey([12, 112, 75, 77, 19, 96, 38, 134, 98, 141, 230, 13, 191, 16, 6, 101, 198, 62, 95, 184, 134, 54, 54, 163, 177, 47, 254, 215, 192, 216, 85, 24]), PublicKey([34, 60, 233, 140, 45, 106, 150, 104, 75, 232, 227, 240, 199, 87, 107, 242, 128, 46, 157, 26, 159, 100, 54, 74, 113, 201, 232, 242, 96, 237, 160, 39]), PublicKey([118, 161, 89, 32, 68, 166, 228, 245, 17, 38, 91, 202, 115, 166, 4, 217, 11, 5, 41, 209, 223, 96, 43, 227, 10, 25, 169, 37, 118, 96, 209, 245])})\n\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::sync::announce::test::announcer_adapts_target_to_reach ... FAILED\n\n failures:\n\n failures:\n node::sync::announce::test::announcer_adapts_target_to_reach\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 396 filtered out; finished in 0.03s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::sync::announce::test::announcer_adapts_target_to_reach' (1117345) panicked at crates/radicle/src/node/sync/announce.rs:971:9:\n assertion `left == right` failed\n left: 2\n right: 3\n```",
"base": "66ab7f35c933278b932657936ce0c52a02962302",
"oid": "ee65406246ab44a4455ca9b38a61c8bfd59d385d",
"timestamp": 1784312032
},
{
"id": "eeec57800be24d12bb9c22679fd3cf897e05962b",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"description": "Respond to review",
"base": "66ab7f35c933278b932657936ce0c52a02962302",
"oid": "86f08be3d6ee58bed8a356bc645ce46d4888c40d",
"timestamp": 1784326540
},
{
"id": "4b3b41f69fd944cd96dc8a7d991dacd1665f4f4d",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"description": "Squash feedback, improve stability of tests.",
"base": "66ab7f35c933278b932657936ce0c52a02962302",
"oid": "9edcb11a1d050b1b6f8e11c265d1359d382870b3",
"timestamp": 1784550902
},
{
"id": "7a7ce40ac4fc5eb4bbc2d3644c358a6efafd9c15",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": "fintohaps"
},
"description": "REVIEW: Fix to wording of `mod signature` docs",
"base": "66ab7f35c933278b932657936ce0c52a02962302",
"oid": "ff4817213ba216a8d0876f4d6740e76a19002de0",
"timestamp": 1784581344
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e"
},
"info_url": "https://cci.rad.levitte.org//7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e.html"
}
Started at: 2026-07-20 23:02:35.332623+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/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 187 issues · 43 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb
✓ Switched to branch patch/24e3c53 at revision 4b3b41f
✓ Branch patch/24e3c53 setup to track rad/patches/24e3c53626bd5e1d4a71c188e8d897f47eddbbcb
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout ff4817213ba216a8d0876f4d6740e76a19002de0
HEAD is now at ff481721 REVIEW: Fix to wording of `mod signature` docs
Exit code: 0
$ rad patch show 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb -p
╭───────────────────────────────────────────────────────────────────────────────────╮
│ Title crypto: Rewrite │
│ Patch 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 9edcb11a1d050b1b6f8e11c265d1359d382870b3 │
│ Base 66ab7f35c933278b932657936ce0c52a02962302 │
│ Branches patch/24e3c53 │
│ Commits ahead 4, 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. │
├───────────────────────────────────────────────────────────────────────────────────┤
│ 9edcb11 cargo/dev: Optimize compute-heavy dependencies │
│ d18c9f6 crypto: Rewrite │
│ 0fd0104 radicle, cli/auth: Let binary crate generate seed │
│ 8ceef3c crypto/test: Diffie-Hellman Fixture │
├───────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 24e3c53 @ 027de89..f8d4ff1 by lorenz z6MkkPv…WX5sTEz 3 days ago │
│ ↑ Revision f0c57fd @ 027de89..6ce9691 by lorenz z6MkkPv…WX5sTEz 3 days ago │
│ ↑ Revision ed140d9 @ 66ab7f3..ee65406 by fintohaps z6Mkire…SQZ3voM 3 days ago │
│ ↑ Revision eeec578 @ 66ab7f3..86f08be by lorenz z6MkkPv…WX5sTEz 2 days ago │
│ ↑ Revision 4b3b41f @ 66ab7f3..9edcb11 by lorenz z6MkkPv…WX5sTEz 8 hours ago │
│ ↑ Revision 7a7ce40 @ 66ab7f3..ff48172 by fintohaps z6Mkire…SQZ3voM 13 seconds ago │
╰───────────────────────────────────────────────────────────────────────────────────╯
commit 9edcb11a1d050b1b6f8e11c265d1359d382870b3
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Mon Jul 6 22:13:56 2026 +0200
cargo/dev: Optimize compute-heavy dependencies
diff --git a/Cargo.toml b/Cargo.toml
index e88f1a671..c6ddbc653 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -96,3 +96,16 @@ wildcard_enum_match_arm = "deny"
inherits = "release"
debug = true
incremental = false
+
+# For dependencies that do not change often and that are compute-heavy
+# at runtime, trade slower builds for faster runtime.
+# This is noticeable when running tests during development.
+[profile.dev.package]
+base64 = { opt-level = 3 }
+chacha20 = { opt-level = 3 }
+curve25519-dalek = { opt-level = 3 }
+ed25519 = { opt-level = 3 }
+mio = { opt-level = 3 }
+multibase = { opt-level = 3 }
+poly1305 = { opt-level = 3 }
+sha2 = { opt-level = 3 }
\ No newline at end of file
commit d18c9f69c66b6cf6d0092b0e9c65ff5202a614a8
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.
Co-authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
diff --git a/Cargo.lock b/Cargo.lock
index f2e73a09c..b200983b7 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"
@@ -699,6 +693,7 @@ dependencies = [
"fiat-crypto",
"rustc_version",
"subtle",
+ "zeroize",
]
[[package]]
@@ -733,6 +728,7 @@ checksum = "f23b157ed1e424149e1af82c6ed8300d41941756f5e9841396fd987d946ab0fe"
dependencies = [
"amplify",
"ec25519",
+ "multibase",
"sha2",
]
@@ -884,7 +880,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 +913,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 +926,10 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519 2.2.3",
+ "serde",
"sha2",
"subtle",
+ "zeroize",
]
[[package]]
@@ -2881,6 +2880,7 @@ dependencies = [
"jsonschema",
"libc",
"log",
+ "multibase",
"nonempty 0.9.0",
"pretty_assertions",
"qcheck",
@@ -2985,7 +2985,6 @@ dependencies = [
"radicle-oid",
"serde",
"serde_json",
- "signature 2.2.0",
"tempfile",
"thiserror 2.0.18",
]
@@ -3013,17 +3012,15 @@ 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",
@@ -4196,7 +4193,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 +4553,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"
@@ -5322,9 +5319,12 @@ 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",
+]
[[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..85e34f8ce 100644
--- a/crates/radicle-crypto/Cargo.toml
+++ b/crates/radicle-crypto/Cargo.toml
@@ -5,39 +5,42 @@ 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"]
+alloc = ["ed25519-dalek/alloc", "curve25519-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", "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"]
[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"] }
[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..56ff4c349 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -1,543 +1,838 @@
-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")]
+/// References to of dalek cryptography crates (see <https://dalek.rs/>)
+/// that this crate depends on. Since both are related to Curve25519
+/// in some way, the "25519" suffix is omitted from the name of the re-export.
+mod dalek {
+ pub(crate) extern crate curve25519_dalek as curve;
+ pub(crate) extern crate ed25519_dalek as ed;
+}
+
+/// Re-exports of the `signature` crate and `ed25519::Signature`
+/// as re-exported by the `ed25519_dalek` crate.
+pub use dalek::ed::ed25519::{Signature, 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>,
+/// A super-trait that requires:
+/// - [`signature::Signer`] to produce the exported [`Signature`] type,
+/// - [`signature::Keypair`] where the associated
+/// [`signature::Keypair::VerifyingKey`] is the
+/// [`VerifyingKey`] defined in this crate, and
+/// - [`AsRef<PublicKey>`] to obtain a reference to the corresponding
+/// [`PublicKey`].
+///
+/// A blanket implementation is provided for all types that satisfy the trait
+/// bounds.
+pub trait Signer
+where
+ Self: signature::Signer<Signature>,
+ Self: signature::Keypair<VerifyingKey = VerifyingKey>,
+ Self: AsRef<PublicKey>,
+{
+ /// Return a reference to the [`PublicKey`].
+ ///
+ /// This is generally satisfied by the [`AsRef<PublicKey>`] instance.
+ 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];
+
+/// Bytes that are intended/thought to correspond to a point on the Edwards25519
+/// curve (but not on its twist).
+///
+/// This is more compact than [`VerifyingKey`] in memory, and easier to handle,
+/// but it is not guaranteed to be a valid point on the curve, so cannot be used
+/// for actual cryptographic operations such as signature verification or
+/// Diffie-Hellman key exchange.
+#[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;
+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);
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (_, bytes) = multibase::decode(s)?;
- let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
+ multibase::encode(multibase::Base::Base58Btc, buf)
+ }
- Ok(Self(sig))
+ /// Encode the public key to a Git reference string:
+ ///
+ /// `refs/namespaces/<public-key>`
+ ///
+ /// and `<public-key>` is encoded in human-readable format
+ /// ([`PublicKey::to_human`]).
+ #[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())
+ }
+
+ /// Encode the public key a Git reference component, which is equivalent to
+ /// the human-readable format ([`PublicKey::to_human`]).
+ #[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 Deref for Signature {
- type Target = ed25519::Signature;
+ /// Decode a [`PublicKey`] from a namespaced Git reference, expected to be
+ /// in the format:
+ ///
+ /// `refs/namespaces/<public-key>/…`
+ ///
+ /// The `<public-key>` is decoded from the human-readable format
+ /// ([`PublicKey::to_human`]).
+ #[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 _;
- fn deref(&self) -> &Self::Target {
- &self.0
+ let name = refstr.namespace().into_inner();
+ Self::from_str(name.as_str())
}
}
-impl From<[u8; 64]> for Signature {
- fn from(bytes: [u8; 64]) -> Self {
- Self(ed25519::Signature::new(bytes))
- }
-}
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::str::FromStr for PublicKey {
+ type Err = PublicKeyError;
-impl TryFrom<&[u8]> for Signature {
- type Error = ed25519::Error;
+ 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);
+/// A (decompressed) point on the Edwards25519 curve (but not on its twist) that
+/// may be used to verify signatures.
+///
+/// It is not as compact as a [`PublicKey`] in memory and requires more costly
+/// verification/initialization, but directly corresponds to one.
+#[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)?;
-
- Ok(edwards25519::ge_scalarmult(&scalar, &ge).to_bytes())
+impl VerifyingKey {
+ #[allow(clippy::wrong_self_convention)] // Name copied from dalek.
+ #[inline]
+ pub(crate) fn to_bytes(&self) -> PublicKeyBytes {
+ self.0.to_bytes()
}
}
-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";
+
+ type Compressed = PublicKey;
- fn deref(&self) -> &Self::Target {
- &self.0
+ 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,
+ },
+}
+
+/// A (decompressed) point on the Edwards25519 curve (but not on its twist) that
+/// may be used to sign data.
+#[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())
+ /// Construct a new [`SigningKey`] from the provided [`Seed`] by "expanding"
+ /// `seed`. This involves hashing `seed` with SHA-512 and clamping the
+ /// resulting 32-byte digest to produce a valid key.
+ ///
+ /// See also `secret_expand` in [RFC 8032, Sec. 6].
+ ///
+ /// [RFC 8032, Sec. 6]: https://datatracker.ietf.org/doc/html/rfc8032#section-6
+ pub fn from_seed(seed: Seed) -> Self {
+ Self(dalek::ed::SigningKey::from_bytes(seed.as_ref()))
+ }
+
+ #[cfg(any(test, all(feature = "test", feature = "alloc")))]
+ pub fn mock(id: usize) -> Self {
+ Self::from_seed(Seed::mock(id))
+ }
+
+ /// Convert this [`SigningKey`] to a 64-byte keypair.
+ pub fn to_keypair_bytes(&self) -> [u8; dalek::ed::KEYPAIR_LENGTH] {
+ self.0.to_keypair_bytes()
+ }
+
+ /// Convert this [`SigningKey`] into a reference to its 32-byte
+ /// representation.
+ 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)
}
-}
-impl From<PublicKey> for String {
- fn from(other: PublicKey) -> Self {
- other.to_human()
+ /// 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())
}
}
-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;
+
+ fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
+ self.diffie_hellman(pk)
+ .ok_or(cyphernet::EcdhError::InvalidPk(
+ cyphernet::EcPkInvalid::default(),
+ ))
+ }
+}
- Self::from_str(name.deref().as_str())
+#[cfg(feature = "qcheck")]
+impl qcheck::Arbitrary for SigningKey {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ SigningKey::mock(usize::arbitrary(g))
}
}
-impl FromStr for PublicKey {
+#[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(feature = "qcheck")]
+impl qcheck::Arbitrary for PublicKey {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ *SigningKey::from_seed(Seed::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;
+
+ /// 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 +840,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 +852,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,
[
diff --git a/crates/radicle-crypto/src/seed.rs b/crates/radicle-crypto/src/seed.rs
new file mode 100644
index 000000000..36a323f0c
--- /dev/null
+++ b/crates/radicle-crypto/src/seed.rs
@@ -0,0 +1,36 @@
+use zeroize::Zeroizing;
+
+#[derive(Clone, Debug)]
+pub struct Seed(Zeroizing<[u8; Self::BYTES]>);
+
+impl Seed {
+ pub const BYTES: usize = 32;
+
+ pub fn new(seed: [u8; Self::BYTES]) -> Self {
+ Seed(Zeroizing::from(seed))
+ }
+
+ #[cfg(any(test, all(feature = "test", feature = "alloc")))]
+ pub fn mock(bytes: usize) -> Self {
+ use alloc::vec::Vec;
+
+ let bytes = core::iter::repeat_n(bytes, Self::BYTES / core::mem::size_of::<usize>())
+ .flat_map(|bytes| bytes.to_be_bytes())
+ .collect::<Vec<_>>();
+
+ Self::new(bytes.try_into().expect("length matches"))
+ }
+}
+
+impl AsRef<[u8; Self::BYTES]> for Seed {
+ fn as_ref(&self) -> &[u8; Self::BYTES] {
+ &self.0
+ }
+}
+
+#[cfg(feature = "qcheck")]
+impl qcheck::Arbitrary for Seed {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ Self::new(qcheck::Arbitrary::arbitrary(g))
+ }
+}
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 4fbf66d7d..9c4145d16 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::{LocalDuration, 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"))]
@@ -66,10 +65,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 {
@@ -293,17 +292,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.
@@ -316,19 +315,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(),
@@ -482,14 +484,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
@@ -538,11 +539,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) => {
@@ -580,13 +577,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);
@@ -628,7 +625,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
@@ -830,7 +827,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.
@@ -912,13 +909,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() {
@@ -964,13 +960,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();
@@ -1087,12 +1083,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,
@@ -1159,7 +1159,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,
@@ -1169,22 +1169,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}");
}
@@ -1256,11 +1256,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,
};
@@ -1289,7 +1287,6 @@ mod logger {
#[cfg(test)]
mod test {
use super::*;
- use crate::crypto::test::signer::MockSigner;
use radicle_protocol::service::ServiceState as _;
@@ -1430,7 +1427,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>,
@@ -1459,7 +1456,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 5f541cd2f..2b7115787 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 {
@@ -2697,14 +2691,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..6783c1fe1 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,53 @@ impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
}
}
+/// This module defines a function to serialize [`Signature`] by
+/// encoding it as a string [multibase format] (using `base58btc` via
+/// [`multibase::encode`] with [`multibase::Base::Base58Btc`]) and
+/// a function to deserialize a [`Signature`] from a string
+/// in multibase format (using [`multibase::decode`]).
+///
+/// The names of these functions are so that the module can be consumed via
+/// `serde_derive`'s `with`, see <https://serde.rs/field-attrs.html#with>. This
+/// is done in particular for variants of [`Action`] and [`Verdict`] that carry
+/// a [`Signature`].
+///
+/// This module exists because, historically, [`Signature`] was a type that did
+/// implement [`serde::Serialize`] and [`serde::Deserialize`], but it was
+/// changed to refer to `ed25519::Signature` in order to maximize compatibility,
+/// which does not implement these traits.
+/// So, for backwards compatibility, this module fills the gap by providing a
+/// way to (se)serialize [`Signature`].
+///
+/// [multibase format]: https://datatracker.ietf.org/doc/draft-multiformats-multibase/
+mod signature {
+ pub fn serialize<Serializer>(
+ signature: &crypto::Signature,
+ serializer: Serializer,
+ ) -> Result<Serializer::Ok, Serializer::Error>
+ where
+ Serializer: serde::Serializer,
+ {
+ serializer.serialize_str(&multibase::encode(
+ multibase::Base::Base58Btc,
+ signature.to_bytes(),
+ ))
+ }
+
+ pub fn deserialize<'de, Deserializer>(
+ deserializer: Deserializer,
+ ) -> Result<crypto::Signature, Deserializer::Error>
+ where
+ Deserializer: serde::Deserializer<'de>,
+ {
+ use serde::Deserialize;
+
+ let (_, bytes) = multibase::decode(String::deserialize(deserializer)?)
+ .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 +1367,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 +1394,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 +1457,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 +2569,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 +2755,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 +2764,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 +2866,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 +2875,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..0fd572781 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::array_distinct::<2, _>();
+
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..01eba76bc 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;
@@ -1523,9 +1522,16 @@ pub(crate) mod properties {
a.push_str(arbitrary::r#gen::<Alias>(1).as_str());
Alias::new(a)
};
+
Self {
- short: (short, arbitrary::vec::<NodeId>(3).into_iter().collect()),
- long: (long, arbitrary::vec::<NodeId>(2).into_iter().collect()),
+ short: (
+ short,
+ arbitrary::array_distinct::<3, _>().into_iter().collect(),
+ ),
+ long: (
+ long,
+ arbitrary::array_distinct::<2, _>().into_iter().collect(),
+ ),
}
}
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..25bfce5a8 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::array_distinct::<10, NodeId>();
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..e9685de76 100644
--- a/crates/radicle/src/node/sync/announce.rs
+++ b/crates/radicle/src/node/sync/announce.rs
@@ -668,13 +668,18 @@ mod test {
#[test]
fn announcer_reached_min_replication_target() {
- let local = arbitrary::r#gen::<NodeId>(0);
- let seeds = arbitrary::set::<NodeId>(10..=10);
- let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
- let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
+ let ids = arbitrary::array_distinct::<6, _>();
+ let local = ids[0];
+ let unsynced = ids[1..4].iter().cloned().collect::<BTreeSet<_>>();
+ let preferred_seeds = ids[4..].iter().cloned().collect::<BTreeSet<_>>();
+
+ let replicas = ReplicationFactor::must_reach(3);
+
+ assert!(replicas.lower_bound() <= unsynced.len() + preferred_seeds.len());
+
let config = AnnouncerConfig::public(
local,
- ReplicationFactor::must_reach(3),
+ replicas,
preferred_seeds.clone(),
BTreeSet::new(),
unsynced.clone(),
@@ -729,17 +734,23 @@ mod test {
#[test]
fn announcer_reached_max_replication_target() {
- let local = arbitrary::r#gen::<NodeId>(0);
- let seeds = arbitrary::set::<NodeId>(10..=10);
- let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
- let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
+ const UPPER: usize = 6;
+
+ let ids = arbitrary::array_distinct::<10, _>();
+ let local = ids[0];
+ let unsynced = ids[1..7].iter().copied().collect::<BTreeSet<_>>();
+ let preferred_seeds = ids[7..].iter().copied().collect::<BTreeSet<_>>();
+
let config = AnnouncerConfig::public(
local,
- ReplicationFactor::range(3, 6),
+ ReplicationFactor::range(3, UPPER),
preferred_seeds.clone(),
BTreeSet::new(),
unsynced.clone(),
);
+
+ assert!(config.replicas.upper_bound().unwrap() <= unsynced.len());
+
let mut announcer = Announcer::new(config).unwrap();
let to_sync = announcer.to_sync();
assert_eq!(to_sync, unsynced.union(&preferred_seeds).copied().collect());
@@ -769,7 +780,7 @@ mod test {
success.as_ref().unwrap().outcome(),
SuccessfulOutcome::MaxReplicationFactor {
preferred: 0,
- synced: 6,
+ synced: UPPER,
}
)
}
@@ -840,10 +851,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 +1571,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..ebaf27581 100644
--- a/crates/radicle/src/node/sync/fetch.rs
+++ b/crates/radicle/src/node/sync/fetch.rs
@@ -490,6 +490,8 @@ impl FetcherConfig {
replicas: ReplicationFactor,
local_node: NodeId,
) -> Self {
+ debug_assert!(!seeds.contains(&local_node));
+
let candidates = seeds
.clone()
.into_iter()
@@ -545,12 +547,15 @@ mod test {
#[test]
fn all_nodes_are_candidates() {
- let local = arbitrary::r#gen::<NodeId>(0);
let replicas = ReplicationFactor::default();
- let seeds = arbitrary::set::<NodeId>(3..=6)
- .into_iter()
- .collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ let ids = arbitrary::array_distinct::<7, _>();
+
+ let local = ids[0];
+ let seeds = ids[1..4].iter().copied().collect::<BTreeSet<_>>();
+
+ let extra_candidates = ids[4..].iter().copied().collect::<BTreeSet<_>>();
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
@@ -574,10 +579,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)
@@ -626,12 +632,14 @@ mod test {
#[test]
fn reaches_target_of_preferred_seeds() {
- let local = arbitrary::r#gen::<NodeId>(0);
+ let ids = arbitrary::array_distinct::<7, _>();
+
+ let local = ids[0];
+ let seeds = ids[1..4].iter().copied().collect::<BTreeSet<_>>();
+ let extra_candidates = ids[4..].iter().copied().collect::<BTreeSet<_>>();
+
let replicas = ReplicationFactor::default();
- let seeds = arbitrary::set::<NodeId>(3..=3)
- .into_iter()
- .collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
@@ -668,17 +676,22 @@ mod test {
#[test]
fn reaches_target_of_replicas() {
- let local = arbitrary::r#gen::<NodeId>(0);
+ let ids = arbitrary::array_distinct::<7, _>();
+
+ let local = ids[0];
+ let seeds = ids[1..4].iter().copied().collect::<BTreeSet<_>>();
+ let extra_candidates = ids[4..].iter().copied().collect::<BTreeSet<_>>();
+
let replicas = ReplicationFactor::must_reach(3);
- let seeds = arbitrary::set::<NodeId>(3..=3)
- .into_iter()
- .collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ 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 24c6bfde5..513900844 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,8 @@ 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 +206,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] 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] 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 +235,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 +321,42 @@ 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());
+ pub fn signer(&self) -> Result<Signer, SignerError> {
+ /// Where to obtain the signer from.
+ enum Source {
+ /// Load the secret key from the keystore without a passphrase.
+ /// This only works if the keystore is not encrypted.
+ KeystorePlain,
+ /// Load the secret key from the keystore using the given
+ /// passphrase to decrypt it.
+ KeystoreEncrypted(Passphrase),
+ /// Use `ssh-agent`.
+ Agent,
}
- if let Some(passphrase) = env::passphrase() {
- let signer = keystore::MemorySigner::load(&self.keystore, Some(passphrase))?;
- return Ok(Device::from(signer).boxed());
- }
+ use Source::*;
- 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())
+ let source = if self.keystore.is_encrypted()? {
+ env::passphrase().map(KeystoreEncrypted).unwrap_or(Agent)
} else {
- Err(SignerError::KeyNotRegistered(self.public_key))
- }
+ KeystorePlain
+ };
+
+ let signer = match source {
+ KeystoreEncrypted(passphrase) => {
+ Signer::Key(crypto::SigningKey::load(&self.keystore, Some(passphrase))?)
+ }
+ KeystorePlain => Signer::Key(crypto::SigningKey::load(&self.keystore, None)?),
+ Agent => Signer::Agent(
+ crypto::ssh::agent::Agent::connect()?.into_signer(
+ self.id()
+ .try_into()
+ .map_err(SignerError::InvalidPublicKey)?,
+ )?,
+ ),
+ };
+
+ Ok(signer)
}
/// Get Radicle home.
@@ -797,7 +803,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 +836,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..ce81c2397 100644
--- a/crates/radicle/src/test/arbitrary.rs
+++ b/crates/radicle/src/test/arbitrary.rs
@@ -65,6 +65,14 @@ pub fn vec<T: Eq + Arbitrary>(size: usize) -> Vec<T> {
vec
}
+fn vec_distinct<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> Vec<T> {
+ set(range).into_iter().collect::<Vec<_>>()
+}
+
+pub fn array_distinct<const N: usize, T: std::fmt::Debug + Eq + Hash + Arbitrary>() -> [T; N] {
+ vec_distinct(N..=N).try_into().unwrap()
+}
+
pub fn nonempty_storage(size: usize) -> MockStorage {
let mut storage = r#gen::<MockStorage>(size);
for _ in 0..size {
@@ -166,11 +174,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 +248,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 0fd0104e8771fa7f2bec64216b3668485ff6b324
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 64f286856..24c6bfde5 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 8ceef3c40571809910e4c207f3e55e95f8834490
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 7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e -v /opt/radcis/ci.rad.levitte.org/cci/state/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/s:/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w:/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w -w /7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/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 byteorder v1.5.0
Downloaded bytesize v2.3.1
Downloaded bytecount v0.6.9
Downloaded crc32fast v1.5.0
Downloaded cbc v0.1.2
Downloaded getrandom v0.2.17
Downloaded addr2line v0.25.1
Downloaded bytes v1.11.1
Downloaded crypto-bigint v0.5.5
Downloaded emojis v0.6.4
Downloaded scopeguard v1.2.0
Downloaded gix-lock v23.0.0
Downloaded bstr v1.12.1
Downloaded qcheck v1.0.0
Downloaded rusty-fork v0.3.1
Downloaded shell-words v1.1.1
Downloaded curve25519-dalek-derive v0.1.1
Downloaded flate2 v1.1.9
Downloaded pkcs8 v0.10.2
Downloaded proc-macro-error-attr3 v3.0.2
Downloaded data-encoding-macro-internal v0.1.17
Downloaded fnv v1.0.7
Downloaded potential_utf v0.1.4
Downloaded pretty_assertions v1.4.1
Downloaded data-encoding v2.10.0
Downloaded crossbeam-utils v0.8.21
Downloaded cypheraddr v0.4.1
Downloaded cyphergraphy v0.3.1
Downloaded ppv-lite86 v0.2.21
Downloaded gix-config-value v0.18.0
Downloaded crypto-common v0.1.7
Downloaded dunce v1.0.5
Downloaded serde_fmt v1.1.0
Downloaded qcheck-macros v1.0.0
Downloaded serde_derive_internals v0.29.1
Downloaded gix-command v0.9.0
Downloaded displaydoc v0.2.5
Downloaded ref-cast-impl v1.0.25
Downloaded regex v1.12.3
Downloaded rsa v0.9.10
Downloaded git-ref-format v0.7.0
Downloaded gix-glob v0.26.0
Downloaded serde_core v1.0.228
Downloaded ed25519 v2.2.3
Downloaded erased-serde v0.4.10
Downloaded escargot v0.5.15
Downloaded radicle-git-ext v0.13.0
Downloaded proptest v1.10.0
Downloaded rustversion v1.0.22
Downloaded scrypt v0.11.0
Downloaded clap_lex v1.1.0
Downloaded radicle-std-ext v0.2.0
Downloaded salsa20 v0.10.2
Downloaded secrecy v0.10.3
Downloaded rustc_version v0.4.1
Downloaded clap v4.6.0
Downloaded filetime v0.2.27
Downloaded ghash v0.5.1
Downloaded document-features v0.2.12
Downloaded convert_case v0.10.0
Downloaded ec25519 v0.1.0
Downloaded ff v0.13.1
Downloaded phf v0.11.3
Downloaded serde_spanned v1.0.4
Downloaded polyval v0.6.2
Downloaded subtle v2.6.1
Downloaded data-encoding-macro v0.1.19
Downloaded equivalent v1.0.2
Downloaded siphasher v1.0.2
Downloaded schemars_derive v1.2.1
Downloaded rand_core v0.9.5
Downloaded rustc-demangle v0.1.27
Downloaded sqlite v0.37.0
Downloaded referencing v0.30.0
Downloaded prodash v31.0.0
Downloaded streaming-iterator v0.1.9
Downloaded sec1 v0.7.3
Downloaded sha2 v0.10.9
Downloaded quick-error v1.2.3
Downloaded smallvec v1.15.1
Downloaded sem_safe v0.2.1
Downloaded group v0.13.0
Downloaded primeorder v0.13.6
Downloaded rand_xorshift v0.4.0
Downloaded shlex v1.3.0
Downloaded ssh-encoding v0.2.0
Downloaded either v1.15.0
Downloaded cpufeatures v0.2.17
Downloaded cfg-if v1.0.4
Downloaded gix-hashtable v0.15.0
Downloaded rand_core v0.6.4
Downloaded ryu v1.0.23
Downloaded same-file v1.0.6
Downloaded serde-untagged v0.1.9
Downloaded sqlite3-sys v0.18.0
Downloaded structured-logger v1.0.5
Downloaded signal-hook-registry v1.4.8
Downloaded sval_buffer v2.17.0
Downloaded snapbox-macros v1.1.0
Downloaded rfc6979 v0.4.0
Downloaded generic-array v0.14.7
Downloaded spin v0.9.8
Downloaded snapbox v1.2.2
Downloaded schemars v1.2.1
Downloaded serde v1.0.228
Downloaded signal-hook v0.3.18
Downloaded env_logger v0.11.9
Downloaded find-msvc-tools v0.1.9
Downloaded gix-negotiate v0.31.0
Downloaded env_filter v1.0.0
Downloaded rand_chacha v0.3.1
Downloaded signal-hook-mio v0.2.5
Downloaded strsim v0.11.1
Downloaded git-ref-format-core v0.6.0
Downloaded gix-actor v0.41.0
Downloaded colorchoice v1.0.5
Downloaded gix-chunk v0.7.1
Downloaded errno v0.3.14
Downloaded dyn-clone v1.0.20
Downloaded signals_receipts v0.2.5
Downloaded ssh-cipher v0.2.0
Downloaded form_urlencoded v1.2.2
Downloaded hash32 v0.3.1
Downloaded signature v1.6.4
Downloaded semver v1.0.27
Downloaded digest v0.10.7
Downloaded gix-date v0.15.3
Downloaded clap_derive v4.6.0
Downloaded const-str v0.4.3
Downloaded proc-macro2 v1.0.106
Downloaded sval_fmt v2.17.0
Downloaded socks5-client v0.4.3
Downloaded sha1 v0.10.6
Downloaded gix-shallow v0.12.0
Downloaded faster-hex v0.10.0
Downloaded fast-glob v0.3.3
Downloaded gix-commitgraph v0.37.0
Downloaded pkg-config v0.3.32
Downloaded ref-cast v1.0.25
Downloaded pastey v0.2.1
Downloaded normalize-line-endings v0.3.0
Downloaded itoa v1.0.17
Downloaded signature v2.2.0
Downloaded quote v1.0.45
Downloaded spki v0.7.3
Downloaded inout v0.1.4
Downloaded siphasher v0.3.11
Downloaded sval_serde v2.17.0
Downloaded universal-hash v0.5.1
Downloaded bit-set v0.8.0
Downloaded stable_deref_trait v1.2.1
Downloaded jobserver v0.1.34
Downloaded value-bag-serde1 v1.12.0
Downloaded cipher v0.4.4
Downloaded fastrand v2.3.0
Downloaded chacha20 v0.9.1
Downloaded git-ref-format-macro v0.7.0
Downloaded gix-fs v0.21.1
Downloaded poly1305 v0.8.0
Downloaded gix-refspec v0.41.0
Downloaded test-log v0.2.19
Downloaded ecdsa v0.16.9
Downloaded email_address v0.2.9
Downloaded simd-adler32 v0.3.8
Downloaded gix-error v0.2.3
Downloaded gix-hash v0.25.0
Downloaded rand_chacha v0.9.0
Downloaded sval_dynamic v2.17.0
Downloaded autocfg v1.5.0
Downloaded gix-credentials v0.38.0
Downloaded serde_derive v1.0.228
Downloaded utf8_iter v1.0.4
Downloaded proc-macro-error3 v3.0.2
Downloaded ssh-agent-lib v0.6.0
Downloaded socket2 v0.5.10
Downloaded block-buffer v0.10.4
Downloaded unit-prefix v0.5.2
Downloaded blowfish v0.9.1
Downloaded ed25519 v1.5.3
Downloaded gix-sec v0.14.0
Downloaded walkdir v2.5.0
Downloaded tinystr v0.8.2
Downloaded num-integer v0.1.46
Downloaded parking_lot_core v0.9.12
Downloaded thiserror-impl v1.0.69
Downloaded unarray v0.1.4
Downloaded aead v0.5.2
Downloaded diff v0.1.13
Downloaded const-oid v0.9.6
Downloaded fluent-uri v0.3.2
Downloaded multibase v0.9.2
Downloaded icu_normalizer_data v2.1.1
Downloaded rand v0.9.2
Downloaded sharded-slab v0.1.7
Downloaded gix-features v0.48.0
Downloaded colored v2.2.0
Downloaded ctr v0.9.2
Downloaded sval v2.17.0
Downloaded getrandom v0.4.3
Downloaded gix-path v0.12.0
Downloaded iana-time-zone v0.1.65
Downloaded opaque-debug v0.3.1
Downloaded yoke-derive v0.8.1
Downloaded human-panic v2.0.6
Downloaded icu_normalizer v2.1.1
Downloaded clap_builder v4.6.0
Downloaded idna_adapter v1.2.1
Downloaded phf_shared v0.11.3
Downloaded num-iter v0.1.45
Downloaded percent-encoding v2.3.2
Downloaded gix-packetline v0.21.3
Downloaded zerovec-derive v0.11.2
Downloaded console v0.16.3
Downloaded elliptic-curve v0.13.8
Downloaded base-x v0.2.11
Downloaded similar v3.1.1
Downloaded memmap2 v0.9.10
Downloaded outref v0.5.2
Downloaded block-padding v0.3.3
Downloaded bcrypt-pbkdf v0.10.0
Downloaded sval_json v2.17.0
Downloaded value-bag-sval2 v1.12.0
Downloaded portable-atomic v1.13.1
Downloaded version_check v0.9.5
Downloaded clap_complete v4.6.0
Downloaded keccak v0.1.6
Downloaded thiserror v2.0.18
Downloaded timeago v0.4.2
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded ascii v1.1.0
Downloaded num v0.4.3
Downloaded thread_local v1.1.9
Downloaded writeable v0.6.2
Downloaded heck v0.5.0
Downloaded zeroize v1.9.0
Downloaded rand v0.8.5
Downloaded parking_lot v0.12.5
Downloaded matchers v0.2.0
Downloaded getrandom v0.3.4
Downloaded uuid-simd v0.8.0
Downloaded xattr v1.6.1
Downloaded tree-sitter-json v0.24.8
Downloaded anstyle-parse v0.2.7
Downloaded gix-utils v0.3.2
Downloaded toml v0.9.12+spec-1.1.0
Downloaded tempfile v3.27.0
Downloaded amplify_syn v2.0.1
Downloaded systemd-journal-logger v2.2.2
Downloaded anstyle-parse v1.0.0
Downloaded icu_properties v2.1.2
Downloaded sval_ref v2.17.0
Downloaded gix-prompt v0.15.0
Downloaded sval_nested v2.17.0
Downloaded noise-framework v0.4.1
Downloaded unicode-display-width v0.3.0
Downloaded thiserror v1.0.69
Downloaded chacha20poly1305 v0.10.1
Downloaded thiserror-impl v2.0.18
Downloaded tree-sitter-language v0.1.7
Downloaded crossbeam-channel v0.5.15
Downloaded anstream v0.6.21
Downloaded litrs v1.0.0
Downloaded base32 v0.4.0
Downloaded gix-revwalk v0.31.0
Downloaded nu-ansi-term v0.50.3
Downloaded test-log-macros v0.2.19
Downloaded typeid v1.0.3
Downloaded tree-sitter-go v0.23.4
Downloaded lock_api v0.4.14
Downloaded amplify_num v0.5.3
Downloaded zerofrom-derive v0.1.6
Downloaded gix-quote v0.7.1
Downloaded borrow-or-share v0.2.4
Downloaded ssh-key v0.6.7
Downloaded vsimd v0.8.0
Downloaded base16ct v0.2.0
Downloaded humantime v2.3.0
Downloaded log v0.4.29
Downloaded pbkdf2 v0.12.2
Downloaded gix-transport v0.57.0
Downloaded base64ct v1.8.3
Downloaded gix-validate v0.11.1
Downloaded base64 v0.22.1
Downloaded p256 v0.13.2
Downloaded zmij v1.0.21
Downloaded hmac v0.12.1
Downloaded gix-url v0.36.0
Downloaded toml_writer v1.0.7+spec-1.1.0
Downloaded gix-revision v0.45.0
Downloaded tracing-log v0.2.0
Downloaded typenum v1.20.1
Downloaded ahash v0.8.12
Downloaded nonempty v0.12.0
Downloaded anstyle v1.0.14
Downloaded ed25519-dalek v2.2.0
Downloaded gix-trace v0.1.19
Downloaded match-lookup v0.1.2
Downloaded zerofrom v0.1.6
Downloaded fancy-regex v0.14.0
Downloaded tinyvec v1.11.0
Downloaded anstream v1.0.0
Downloaded adler2 v2.0.1
Downloaded num-cmp v0.1.0
Downloaded is_terminal_polyfill v1.70.2
Downloaded utf8parse v0.2.2
Downloaded yoke v0.8.1
Downloaded tinyvec_macros v0.1.1
Downloaded gix-traverse v0.57.0
Downloaded gix-tempfile v23.0.0
Downloaded nonempty v0.9.0
Downloaded base256emoji v1.0.2
Downloaded anstyle-query v1.1.5
Downloaded maybe-async v0.2.10
Downloaded once_cell v1.21.4
Downloaded wait-timeout v0.2.1
Downloaded crossterm v0.29.0
Downloaded backtrace v0.3.76
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded gix-protocol v0.61.0
Downloaded lazy_static v1.5.0
Downloaded value-bag v1.12.0
Downloaded gix-diff v0.63.0
Downloaded url v2.5.8
Downloaded tracing-core v0.1.36
Downloaded radicle-surf v0.28.0
Downloaded memchr v2.8.0
Downloaded tree-sitter-highlight v0.24.7
Downloaded yansi v1.0.1
Downloaded tree-sitter-html v0.23.2
Downloaded derive_more v2.1.1
Downloaded lexopt v0.3.2
Downloaded serde_json v1.0.149
Downloaded pem-rfc7468 v0.7.0
Downloaded unicode-ident v1.0.24
Downloaded cc v1.2.57
Downloaded fraction v0.15.3
Downloaded der v0.7.10
Downloaded derive_more-impl v2.1.1
Downloaded pin-project-lite v0.2.17
Downloaded bit-vec v0.8.0
Downloaded arc-swap v1.9.1
Downloaded curve25519-dalek v4.1.3
Downloaded uuid v1.22.0
Downloaded num-complex v0.4.6
Downloaded synstructure v0.13.2
Downloaded base64 v0.21.7
Downloaded pkcs1 v0.7.5
Downloaded num-rational v0.4.2
Downloaded miniz_oxide v0.8.9
Downloaded amplify v4.9.0
Downloaded gix-object v0.60.0
Downloaded amplify_derive v4.0.1
Downloaded aes v0.8.4
Downloaded indicatif v0.18.4
Downloaded litemap v0.8.1
Downloaded icu_locale_core v2.1.1
Downloaded zerotrie v0.2.3
Downloaded anyhow v1.0.102
Downloaded num-traits v0.2.19
Downloaded bitflags v2.11.0
Downloaded gix-odb v0.80.0
Downloaded jiff-static v0.2.23
Downloaded hashbrown v0.16.1
Downloaded tree-sitter-css v0.23.2
Downloaded num-bigint v0.4.6
Downloaded libm v0.2.16
Downloaded inquire v0.9.4
Downloaded num-bigint-dig v0.8.6
Downloaded indexmap v2.13.0
Downloaded icu_provider v2.1.1
Downloaded gix-pack v0.70.0
Downloaded aes-gcm v0.10.3
Downloaded tar v0.4.45
Downloaded unicode-normalization v0.1.25
Downloaded p384 v0.13.1
Downloaded gix-ref v0.63.0
Downloaded icu_collections v2.1.1
Downloaded unicode-segmentation v1.12.0
Downloaded chrono v0.4.44
Downloaded jsonschema v0.30.0
Downloaded cyphernet v0.5.4
Downloaded aho-corasick v1.1.4
Downloaded regex-syntax v0.8.10
Downloaded heapless v0.8.0
Downloaded mio v1.1.1
Downloaded p521 v0.13.3
Downloaded syn v1.0.109
Downloaded rustix v1.1.4
Downloaded tree-sitter-python v0.23.6
Downloaded zerovec v0.11.5
Downloaded zerocopy v0.8.42
Downloaded idna v1.1.0
Downloaded vcpkg v0.2.15
Downloaded itertools v0.14.0
Downloaded icu_properties_data v2.1.2
Downloaded tree-sitter-c v0.23.4
Downloaded zlib-rs v0.6.5
Downloaded tree-sitter v0.24.7
Downloaded git2 v0.21.0
Downloaded unicode-width v0.2.2
Downloaded tracing-subscriber v0.3.23
Downloaded gimli v0.32.3
Downloaded sysinfo v0.37.2
Downloaded tracing v0.1.44
Downloaded tree-sitter-bash v0.23.3
Downloaded tree-sitter-md v0.3.2
Downloaded regex-automata v0.4.14
Downloaded syn v2.0.117
Downloaded object v0.37.3
Downloaded tree-sitter-rust v0.23.3
Downloaded bloomy v1.2.0
Downloaded sha1-checked v0.10.0
Downloaded tree-sitter-ruby v0.23.1
Downloaded sha3 v0.10.8
Downloaded tree-sitter-typescript v0.23.2
Downloaded libz-sys v1.1.25
Downloaded jiff v0.2.23
Downloaded libc v0.2.183
Downloaded tokio v1.50.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 syn v2.0.117
Compiling generic-array v0.14.7
Checking getrandom v0.2.17
Checking typenum v1.20.1
Checking rand_core v0.6.4
Checking memchr v2.8.0
Compiling jobserver v0.1.34
Compiling find-msvc-tools v0.1.9
Compiling shlex v1.3.0
Compiling cc v1.2.57
Checking subtle v2.6.1
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 fastrand v2.3.0
Compiling parking_lot_core v0.9.12
Checking regex-automata v0.4.14
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
Compiling crc32fast v1.5.0
Checking tinyvec_macros v0.1.1
Checking itoa v1.0.17
Checking gix-trace v0.1.19
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
Checking log v0.4.29
Checking prodash v31.0.0
Compiling serde_derive v1.0.228
Checking bstr v1.12.1
Compiling thiserror-impl v2.0.18
Compiling libm v0.2.16
Checking gix-validate v0.11.1
Checking zlib-rs v0.6.5
Compiling autocfg v1.5.0
Compiling num-traits v0.2.19
Compiling heapless v0.8.0
Compiling rustix v1.1.4
Checking gix-path v0.12.0
Compiling pkg-config v0.3.32
Checking gix-features v0.48.0
Checking hash32 v0.3.1
Checking linux-raw-sys v0.12.1
Compiling getrandom v0.4.3
Checking faster-hex v0.10.0
Compiling zerocopy v0.8.42
Compiling zmij v1.0.21
Checking zeroize v1.9.0
Checking once_cell v1.21.4
Checking equivalent v1.0.2
Compiling serde_json v1.0.149
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 sha1 v0.10.6
Checking inout v0.1.4
Checking sha2 v0.10.9
Checking cipher v0.4.4
Checking sha1-checked v0.10.0
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
Compiling vcpkg v0.2.15
Checking dyn-clone v1.0.20
Compiling thiserror v1.0.69
Compiling syn v1.0.109
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
Checking rand_chacha v0.3.1
Compiling schemars_derive v1.2.1
Checking crypto-bigint v0.5.5
Compiling data-encoding v2.10.0
Checking schemars v1.2.1
Compiling data-encoding-macro-internal v0.1.17
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
Compiling semver v1.0.27
Checking signature v1.6.4
Checking const-str v0.4.3
Checking ed25519 v1.5.3
Checking data-encoding-macro v0.1.19
Compiling rustc_version v0.4.1
Compiling amplify_derive v4.0.1
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
Compiling curve25519-dalek v4.1.3
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
Checking base64ct v1.8.3
Compiling num-bigint-dig v0.8.6
Checking ghash v0.5.1
Checking cyphergraphy v0.3.1
Checking pkcs8 v0.10.2
Checking pem-rfc7468 v0.7.0
Checking pbkdf2 v0.12.2
Checking aes v0.8.4
Checking ctr v0.9.2
Compiling sqlite3-src v0.7.0
Compiling curve25519-dalek-derive v0.1.1
Checking keccak v0.1.6
Checking aes-gcm v0.10.3
Checking sha3 v0.10.8
Checking ssh-encoding v0.2.0
Checking pkcs1 v0.7.5
Checking ed25519 v2.2.3
Checking cbc v0.1.2
Checking blowfish v0.9.1
Checking base32 v0.4.0
Checking bcrypt-pbkdf v0.10.0
Checking cypheraddr v0.4.1
Checking rsa v0.9.10
Checking ssh-cipher v0.2.0
Checking ed25519-dalek v2.2.0
Checking p384 v0.13.1
Checking p521 v0.13.3
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 jiff v0.2.23
Checking anstyle-query v1.1.5
Checking utf8parse v0.2.2
Compiling zerofrom-derive v0.1.6
Checking zerofrom v0.1.6
Compiling yoke-derive v0.8.1
Checking radicle-localtime v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-localtime)
Checking errno v0.3.14
Checking anstyle v1.0.14
Checking is_terminal_polyfill v1.70.2
Checking gix-date v0.15.3
Checking colorchoice v1.0.5
Checking yoke v0.8.1
Checking gix-actor v0.41.0
Checking gix-hashtable v0.15.0
Checking nonempty v0.9.0
Checking siphasher v1.0.2
Checking gix-object v0.60.0
Checking radicle-git-metadata v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-metadata)
Compiling zerovec-derive v0.11.2
Checking radicle-dag v0.10.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-dag)
Checking memmap2 v0.9.10
Checking nonempty v0.12.0
Checking radicle-git-ref-format v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-ref-format)
Checking gix-chunk v0.7.1
Compiling displaydoc v0.2.5
Checking zerovec v0.11.5
Checking base64 v0.21.7
Compiling radicle v0.24.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle)
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 anstream v1.0.0
Checking gix-revwalk v0.31.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 potential_utf v0.1.4
Checking icu_locale_core v2.1.1
Checking zerotrie v0.2.3
Compiling icu_normalizer_data v2.1.1
Checking either v1.15.0
Checking shell-words v1.1.1
Compiling icu_properties_data v2.1.2
Compiling unicode-segmentation v1.12.0
Compiling signal-hook v0.3.18
Checking iana-time-zone v0.1.65
Compiling convert_case v0.10.0
Checking chrono v0.4.44
Checking gix-command v0.9.0
Checking icu_provider v2.1.1
Checking icu_collections v2.1.1
Checking radicle-signals v0.11.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/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
Checking gimli v0.32.3
Compiling portable-atomic v1.13.1
Compiling litrs v1.0.0
Checking adler2 v2.0.1
Checking unicode-width v0.2.2
Checking miniz_oxide v0.8.9
Compiling document-features v0.2.12
Checking addr2line v0.25.1
Checking derive_more v2.1.1
Checking gix-prompt v0.15.0
Checking icu_properties v2.1.2
Checking signal-hook-mio v0.2.5
Checking icu_normalizer v2.1.1
Checking radicle-log v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/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-shallow v0.12.0
Checking gix-ref v0.63.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
Compiling simd-adler32 v0.3.8
Checking unit-prefix v0.5.2
Checking utf8_iter v1.0.4
Compiling getrandom v0.3.4
Checking idna v1.1.0
Checking indicatif v0.18.4
Compiling proc-macro-error3 v3.0.2
Checking gix-protocol v0.61.0
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 tar v0.4.45
Compiling flate2 v1.1.9
Compiling git-ref-format-macro v0.7.0
Checking snapbox-macros v1.1.0
Checking salsa20 v0.10.2
Checking similar v3.1.1
Checking normalize-line-endings v0.3.0
Checking streaming-iterator v0.1.9
Checking clap_lex v1.1.0
Checking strsim v0.11.1
Compiling heck v0.5.0
Compiling crossbeam-utils v0.8.21
Checking siphasher v0.3.11
Checking bloomy v1.2.0
Compiling clap_derive v4.6.0
Checking clap_builder v4.6.0
Checking sqlite3-sys v0.18.0
Checking snapbox v1.2.2
Checking sqlite v0.37.0
Checking radicle-crypto v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-crypto)
Compiling radicle-surf v0.28.0
Checking scrypt v0.11.0
Checking git-ref-format v0.7.0
Checking form_urlencoded v1.2.2
Checking systemd-journal-logger v2.2.2
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-python v0.23.6
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-css v0.23.2
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-ruby v0.23.1
Checking serde_spanned v1.0.4
Checking toml_datetime v0.7.5+spec-1.1.0
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 url v2.5.8
Checking clap v4.6.0
Checking sysinfo v0.37.2
Compiling radicle-node v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-node)
Checking yansi v1.0.1
Compiling radicle-cli v0.21.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli)
Checking diff v0.1.13
Checking pretty_assertions v1.4.1
Checking human-panic v2.0.6
Checking crossbeam-channel v0.5.15
Checking clap_complete v4.6.0
Checking structured-logger v1.0.5
Checking radicle-systemd v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.7
Checking itertools v0.14.0
Checking socket2 v0.5.10
Checking lexopt v0.3.2
Compiling escargot v0.5.15
Checking timeago v0.4.2
Checking humantime v2.3.0
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 outref v0.5.2
Checking num-rational v0.4.2
Checking vsimd v0.8.0
Compiling radicle-remote-helper v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-remote-helper)
Checking num v0.4.3
Checking quick-error v1.2.3
Checking fnv v1.0.7
Checking uuid-simd v0.8.0
Checking rusty-fork v0.3.1
Checking fraction v0.15.3
Checking test-log v0.2.19
Checking phf v0.11.3
Checking referencing v0.30.0
Checking rand_xorshift v0.4.0
Checking rand v0.9.2
Checking rand_chacha v0.9.0
Checking fancy-regex v0.14.0
Checking email_address v0.2.9
Checking bytecount v0.6.9
Checking base64 v0.22.1
Checking unarray v0.1.4
Checking num-cmp v0.1.0
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 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-windows)
Checking git2 v0.21.0
Checking radicle-oid v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-oid)
Checking radicle-term v0.18.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-term)
Checking radicle-git-ext v0.13.0
Checking radicle-cob v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cob)
Checking radicle-core v0.3.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-core)
Checking radicle-cli-test v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli-test)
Checking radicle-fetch v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-fetch)
Checking radicle-protocol v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-protocol)
Checking radicle-schemars v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-schemars)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 42.49s
+ cargo build --all-targets --workspace
Compiling libc v0.2.183
Compiling cfg-if v1.0.4
Compiling serde_core v1.0.228
Compiling typenum v1.20.1
Compiling memchr v2.8.0
Compiling shlex v1.3.0
Compiling getrandom v0.2.17
Compiling jobserver v0.1.34
Compiling subtle v2.6.1
Compiling rand_core v0.6.4
Compiling const-oid v0.9.6
Compiling cc v1.2.57
Compiling regex-syntax v0.8.10
Compiling aho-corasick v1.1.4
Compiling serde v1.0.228
Compiling smallvec v1.15.1
Compiling zeroize v1.9.0
Compiling regex-automata v0.4.14
Compiling generic-array v0.14.7
Compiling crypto-common v0.1.7
Compiling block-buffer v0.10.4
Compiling cpufeatures v0.2.17
Compiling digest v0.10.7
Compiling bitflags v2.11.0
Compiling thiserror v2.0.18
Compiling fastrand v2.3.0
Compiling scopeguard v1.2.0
Compiling lock_api v0.4.14
Compiling parking_lot_core v0.9.12
Compiling stable_deref_trait v1.2.1
Compiling typeid v1.0.3
Compiling tinyvec_macros v0.1.1
Compiling erased-serde v0.4.10
Compiling itoa v1.0.17
Compiling parking_lot v0.12.5
Compiling gix-trace v0.1.19
Compiling tinyvec v1.11.0
Compiling serde_fmt v1.1.0
Compiling hashbrown v0.16.1
Compiling value-bag-serde1 v1.12.0
Compiling unicode-normalization v0.1.25
Compiling byteorder v1.5.0
Compiling crc32fast v1.5.0
Compiling gix-utils v0.3.2
Compiling value-bag v1.12.0
Compiling same-file v1.0.6
Compiling bstr v1.12.1
Compiling log v0.4.29
Compiling walkdir v2.5.0
Compiling prodash v31.0.0
Compiling gix-validate v0.11.1
Compiling gix-path v0.12.0
Compiling zlib-rs v0.6.5
Compiling libm v0.2.16
Compiling hash32 v0.3.1
Compiling linux-raw-sys v0.12.1
Compiling rustix v1.1.4
Compiling num-traits v0.2.19
Compiling heapless v0.8.0
Compiling block-padding v0.3.3
Compiling inout v0.1.4
Compiling faster-hex v0.10.0
Compiling getrandom v0.4.3
Compiling sha2 v0.10.9
Compiling sha1 v0.10.6
Compiling sha1-checked v0.10.0
Compiling cipher v0.4.4
Compiling zerocopy v0.8.42
Compiling gix-features v0.48.0
Compiling equivalent v1.0.2
Compiling once_cell v1.21.4
Compiling gix-hash v0.25.0
Compiling indexmap v2.13.0
Compiling zmij v1.0.21
Compiling der v0.7.10
Compiling spin v0.9.8
Compiling lazy_static v1.5.0
Compiling ref-cast v1.0.25
Compiling dyn-clone v1.0.20
Compiling libz-sys v1.1.25
Compiling serde_json v1.0.149
Compiling tempfile v3.27.0
Compiling num-integer v0.1.46
Compiling hmac v0.12.1
Compiling universal-hash v0.5.1
Compiling opaque-debug v0.3.1
Compiling thiserror v1.0.69
Compiling spki v0.7.3
Compiling libgit2-sys v0.18.4+1.9.3
Compiling signature v2.2.0
Compiling ppv-lite86 v0.2.21
Compiling ff v0.13.1
Compiling base16ct v0.2.0
Compiling sec1 v0.7.3
Compiling group v0.13.0
Compiling schemars v1.2.1
Compiling rand_chacha v0.3.1
Compiling crypto-bigint v0.5.5
Compiling rand v0.8.5
Compiling num-iter v0.1.45
Compiling aead v0.5.2
Compiling signature v1.6.4
Compiling elliptic-curve v0.13.8
Compiling const-str v0.4.3
Compiling data-encoding v2.10.0
Compiling base256emoji v1.0.2
Compiling ed25519 v1.5.3
Compiling data-encoding-macro v0.1.19
Compiling poly1305 v0.8.0
Compiling rfc6979 v0.4.0
Compiling chacha20 v0.9.1
Compiling gix-error v0.2.3
Compiling amplify_num v0.5.3
Compiling base-x v0.2.11
Compiling ascii v1.1.0
Compiling multibase v0.9.2
Compiling ecdsa v0.16.9
Compiling ec25519 v0.1.0
Compiling primeorder v0.13.6
Compiling git-ref-format-core v0.6.0
Compiling polyval v0.6.2
Compiling base64ct v1.8.3
Compiling amplify v4.9.0
Compiling pem-rfc7468 v0.7.0
Compiling cyphergraphy v0.3.1
Compiling ghash v0.5.1
Compiling pkcs8 v0.10.2
Compiling pbkdf2 v0.12.2
Compiling aes v0.8.4
Compiling ctr v0.9.2
Compiling sqlite3-src v0.7.0
Compiling keccak v0.1.6
Compiling sha3 v0.10.8
Compiling curve25519-dalek v4.1.3
Compiling aes-gcm v0.10.3
Compiling pkcs1 v0.7.5
Compiling ssh-encoding v0.2.0
Compiling num-bigint-dig v0.8.6
Compiling ed25519 v2.2.3
Compiling blowfish v0.9.1
Compiling cbc v0.1.2
Compiling base32 v0.4.0
Compiling cypheraddr v0.4.1
Compiling ssh-cipher v0.2.0
Compiling bcrypt-pbkdf v0.10.0
Compiling ed25519-dalek v2.2.0
Compiling rsa v0.9.10
Compiling p384 v0.13.1
Compiling p256 v0.13.2
Compiling p521 v0.13.3
Compiling chacha20poly1305 v0.10.1
Compiling qcheck v1.0.0
Compiling ssh-key v0.6.7
Compiling noise-framework v0.4.1
Compiling socks5-client v0.4.3
Compiling secrecy v0.10.3
Compiling percent-encoding v2.3.2
Compiling cyphernet v0.5.4
Compiling utf8parse v0.2.2
Compiling anstyle-query v1.1.5
Compiling jiff v0.2.23
Compiling ssh-agent-lib v0.6.0
Compiling zerofrom v0.1.6
Compiling radicle-localtime v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-localtime)
Compiling errno v0.3.14
Compiling colorchoice v1.0.5
Compiling is_terminal_polyfill v1.70.2
Compiling anstyle v1.0.14
Compiling yoke v0.8.1
Compiling gix-hashtable v0.15.0
Compiling nonempty v0.9.0
Compiling siphasher v1.0.2
Compiling tree-sitter-language v0.1.7
Compiling gix-date v0.15.3
Compiling gix-actor v0.41.0
Compiling gix-object v0.60.0
Compiling radicle-dag v0.10.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-dag)
Compiling radicle-git-metadata v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-metadata)
Compiling memmap2 v0.9.10
Compiling nonempty v0.12.0
Compiling zerovec v0.11.5
Compiling radicle-git-ref-format v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-ref-format)
Compiling gix-chunk v0.7.1
Compiling radicle v0.24.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle)
Compiling base64 v0.21.7
Compiling gix-commitgraph v0.37.0
Compiling anstyle-parse v1.0.0
Compiling serde-untagged v0.1.9
Compiling bytesize v2.3.1
Compiling dunce v1.0.5
Compiling adler2 v2.0.1
Compiling fast-glob v0.3.3
Compiling anstream v1.0.0
Compiling gix-revwalk v0.31.0
Compiling gix-fs v0.21.1
Compiling mio v1.1.1
Compiling sem_safe v0.2.1
Compiling signals_receipts v0.2.5
Compiling gix-tempfile v23.0.0
Compiling tinystr v0.8.2
Compiling gix-quote v0.7.1
Compiling litemap v0.8.1
Compiling unicode-segmentation v1.12.0
Compiling writeable v0.6.2
Compiling icu_locale_core v2.1.1
Compiling potential_utf v0.1.4
Compiling zerotrie v0.2.3
Compiling iana-time-zone v0.1.65
Compiling either v1.15.0
Compiling shell-words v1.1.1
Compiling icu_provider v2.1.1
Compiling gix-command v0.9.0
Compiling chrono v0.4.44
Compiling icu_collections v2.1.1
Compiling convert_case v0.10.0
Compiling radicle-signals v0.11.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-signals)
Compiling signal-hook-registry v1.4.8
Compiling colored v2.2.0
Compiling signal-hook v0.3.18
Compiling derive_more-impl v2.1.1
Compiling icu_properties_data v2.1.2
Compiling icu_normalizer_data v2.1.1
Compiling gix-lock v23.0.0
Compiling gix-url v0.36.0
Compiling gix-config-value v0.18.0
Compiling gix-sec v0.14.0
Compiling gimli v0.32.3
Compiling unicode-width v0.2.2
Compiling gix-prompt v0.15.0
Compiling derive_more v2.1.1
Compiling icu_normalizer v2.1.1
Compiling icu_properties v2.1.2
Compiling addr2line v0.25.1
Compiling signal-hook-mio v0.2.5
Compiling object v0.37.3
Compiling radicle-log v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-log)
Compiling gix-revision v0.45.0
Compiling gix-traverse v0.57.0
Compiling miniz_oxide v0.8.9
Compiling gix-diff v0.63.0
Compiling anstyle-parse v0.2.7
Compiling gix-glob v0.26.0
Compiling gix-packetline v0.21.3
Compiling rustc-demangle v0.1.27
Compiling backtrace v0.3.76
Compiling gix-transport v0.57.0
Compiling gix-refspec v0.41.0
Compiling anstream v0.6.21
Compiling sqlite3-sys v0.18.0
Compiling gix-pack v0.70.0
Compiling sqlite v0.37.0
Compiling arc-swap v1.9.1
Compiling radicle-crypto v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-crypto)
Compiling crossterm v0.29.0
Compiling idna_adapter v1.2.1
Compiling gix-credentials v0.38.0
Compiling portable-atomic v1.13.1
Compiling console v0.16.3
Compiling gix-shallow v0.12.0
Compiling gix-ref v0.63.0
Compiling gix-negotiate v0.31.0
Compiling regex v1.12.3
Compiling tree-sitter v0.24.7
Compiling utf8_iter v1.0.4
Compiling unit-prefix v0.5.2
Compiling indicatif v0.18.4
Compiling gix-protocol v0.61.0
Compiling idna v1.1.0
Compiling gix-odb v0.80.0
Compiling inquire v0.9.4
Compiling xattr v1.6.1
Compiling unicode-display-width v0.3.0
Compiling uuid v1.22.0
Compiling filetime v0.2.27
Compiling bytes v1.11.1
Compiling tar v0.4.45
Compiling git-ref-format-macro v0.7.0
Compiling anyhow v1.0.102
Compiling flate2 v1.1.9
Compiling getrandom v0.3.4
Compiling snapbox-macros v1.1.0
Compiling salsa20 v0.10.2
Compiling clap_lex v1.1.0
Compiling similar v3.1.1
Compiling streaming-iterator v0.1.9
Compiling strsim v0.11.1
Compiling siphasher v0.3.11
Compiling normalize-line-endings v0.3.0
Compiling bloomy v1.2.0
Compiling clap_builder v4.6.0
Compiling snapbox v1.2.2
Compiling scrypt v0.11.0
Compiling radicle-surf v0.28.0
Compiling git-ref-format v0.7.0
Compiling form_urlencoded v1.2.2
Compiling systemd-journal-logger v2.2.2
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-css v0.23.2
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-python v0.23.6
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-md v0.3.2
Compiling toml_datetime v0.7.5+spec-1.1.0
Compiling serde_spanned v1.0.4
Compiling toml_writer v1.0.7+spec-1.1.0
Compiling pin-project-lite v0.2.17
Compiling radicle-std-ext v0.2.0
Compiling toml v0.9.12+spec-1.1.0
Compiling tokio v1.50.0
Compiling url v2.5.8
Compiling clap v4.6.0
Compiling crossbeam-utils v0.8.21
Compiling sysinfo v0.37.2
Compiling radicle-cli v0.21.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli)
Compiling yansi v1.0.1
Compiling diff v0.1.13
Compiling radicle-node v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-node)
Compiling pretty_assertions v1.4.1
Compiling human-panic v2.0.6
Compiling crossbeam-channel v0.5.15
Compiling clap_complete v4.6.0
Compiling structured-logger v1.0.5
Compiling radicle-systemd v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-systemd)
Compiling tree-sitter-highlight v0.24.7
Compiling itertools v0.14.0
Compiling socket2 v0.5.10
Compiling humantime v2.3.0
Compiling timeago v0.4.2
Compiling lexopt v0.3.2
Compiling bit-vec v0.8.0
Compiling bit-set v0.8.0
Compiling escargot v0.5.15
Compiling rand_core v0.9.5
Compiling num-bigint v0.4.6
Compiling num-complex v0.4.6
Compiling env_filter v1.0.0
Compiling borrow-or-share v0.2.4
Compiling fluent-uri v0.3.2
Compiling git2 v0.21.0
Compiling num-rational v0.4.2
Compiling env_logger v0.11.9
Compiling num v0.4.3
Compiling ahash v0.8.12
Compiling phf_shared v0.11.3
Compiling wait-timeout v0.2.1
Compiling vsimd v0.8.0
Compiling fnv v1.0.7
Compiling radicle-remote-helper v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-remote-helper)
Compiling outref v0.5.2
Compiling quick-error v1.2.3
Compiling rusty-fork v0.3.1
Compiling uuid-simd v0.8.0
Compiling test-log v0.2.19
Compiling phf v0.11.3
Compiling referencing v0.30.0
Compiling fraction v0.15.3
Compiling rand_xorshift v0.4.0
Compiling rand_chacha v0.9.0
Compiling rand v0.9.2
Compiling fancy-regex v0.14.0
Compiling email_address v0.2.9
Compiling radicle-oid v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-oid)
Compiling radicle-term v0.18.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-term)
Compiling radicle-core v0.3.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-core)
Compiling radicle-cob v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cob)
Compiling radicle-git-ext v0.13.0
Compiling radicle-cli-test v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli-test)
Compiling base64 v0.22.1
Compiling unarray v0.1.4
Compiling num-cmp v0.1.0
Compiling bytecount v0.6.9
Compiling proptest v1.10.0
Compiling jsonschema v0.30.0
Compiling emojis v0.6.4
Compiling radicle-fetch v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-fetch)
Compiling radicle-protocol v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-protocol)
Compiling radicle-schemars v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-schemars)
Compiling radicle-windows v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-windows)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 57.37s
+ cargo doc --workspace --no-deps --all-features
Downloading crates ...
Downloaded dhat v0.3.3
Downloaded mintex v0.1.4
Downloaded rustc-hash v1.1.0
Downloaded thousands v0.2.0
Checking regex-automata v0.4.14
Compiling num-traits v0.2.19
Checking once_cell v1.21.4
Compiling syn v1.0.109
Checking tempfile v3.27.0
Checking rusty-fork v0.3.1
Checking radicle-localtime v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-localtime)
Checking radicle-git-metadata v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-metadata)
Checking radicle-dag v0.10.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-dag)
Checking num-integer v0.1.46
Checking proptest v1.10.0
Checking num-iter v0.1.45
Checking num-bigint-dig v0.8.6
Compiling amplify_syn v2.0.1
Checking bstr v1.12.1
Checking gix-validate v0.11.1
Checking gix-path v0.12.0
Checking gix-error v0.2.3
Checking git-ref-format-core v0.6.0
Compiling amplify_derive v4.0.1
Checking gix-features v0.48.0
Checking rsa v0.9.10
Checking gix-hash v0.25.0
Checking radicle-git-ref-format v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-ref-format)
Checking gix-date v0.15.3
Checking radicle-oid v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-oid)
Checking ssh-key v0.6.7
Checking gix-hashtable v0.15.0
Checking gix-actor v0.41.0
Checking gix-chunk v0.7.1
Checking gix-object v0.60.0
Checking gix-commitgraph v0.37.0
Checking gix-fs v0.21.1
Checking gix-tempfile v23.0.0
Checking ssh-agent-lib v0.6.0
Checking gix-revwalk v0.31.0
Checking gix-quote v0.7.1
Checking gix-command v0.9.0
Checking chrono v0.4.44
Checking radicle-signals v0.11.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-signals)
Checking gix-lock v23.0.0
Checking amplify v4.9.0
Checking gix-url v0.36.0
Checking gix-config-value v0.18.0
Checking inquire v0.9.4
Checking cyphergraphy v0.3.1
Checking gix-prompt v0.15.0
Checking gix-revision v0.45.0
Checking cypheraddr v0.4.1
Checking noise-framework v0.4.1
Checking gix-traverse v0.57.0
Checking gix-diff v0.63.0
Checking socks5-client v0.4.3
Checking gix-glob v0.26.0
Checking gix-packetline v0.21.3
Checking cyphernet v0.5.4
Checking radicle-crypto v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-crypto)
Checking regex v1.12.3
Checking idna v1.1.0
Checking gix-transport v0.57.0
Checking gix-refspec v0.41.0
Checking tree-sitter v0.24.7
Checking radicle-cob v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cob)
Checking radicle-core v0.3.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-core)
Checking gix-pack v0.70.0
Checking radicle-log v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-log)
Checking git-ref-format v0.7.0
Checking gix-credentials v0.38.0
Checking gix-shallow v0.12.0
Checking gix-ref v0.63.0
Checking gix-negotiate v0.31.0
Checking radicle v0.24.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle)
Checking radicle-git-ext v0.13.0
Checking url v2.5.8
Checking gix-protocol v0.61.0
Checking radicle-term v0.18.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-term)
Checking gix-odb v0.80.0
Checking uuid v1.22.0
Checking radicle-surf v0.28.0
Checking human-panic v2.0.6
Checking tree-sitter-toml-ng v0.6.0
Checking tree-sitter-highlight v0.24.7
Checking rustc-hash v1.1.0
Checking thousands v0.2.0
Checking mintex v0.1.4
Compiling radicle-node v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-node)
Checking dhat v0.3.3
Checking radicle-systemd v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-systemd)
Documenting radicle-cli-test v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli-test)
Documenting radicle-systemd v0.13.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-systemd)
Documenting radicle v0.24.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle)
Documenting radicle-cob v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cob)
Documenting radicle-core v0.3.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-core)
Documenting radicle-term v0.18.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-term)
Documenting radicle-crypto v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-crypto)
Documenting radicle-log v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-log)
Documenting radicle-signals v0.11.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-signals)
Documenting radicle-oid v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-oid)
Documenting radicle-git-ref-format v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-ref-format)
Documenting radicle-localtime v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-localtime)
Documenting radicle-git-metadata v0.2.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-git-metadata)
Documenting radicle-dag v0.10.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-dag)
Documenting radicle-windows v0.1.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-windows)
Checking radicle-fetch v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-fetch)
Checking radicle-cli v0.21.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli)
Documenting radicle-cli v0.21.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-cli)
Documenting radicle-schemars v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-schemars)
Checking radicle-protocol v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-protocol)
Documenting radicle-protocol v0.8.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-protocol)
Documenting radicle-node v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-node)
Documenting radicle-fetch v0.20.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-fetch)
Documenting radicle-remote-helper v0.17.0 (/7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/crates/radicle-remote-helper)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.37s
Generated /7777e6f6-6e1a-4d60-ba5b-dd77a5e1294e/w/target/doc/radicle/index.html and 21 other files
+ cargo test --workspace --no-fail-fast
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.23s
Running unittests src/lib.rs (target/debug/deps/radicle-5a0242ec3ad6647c)
running 400 tests
test canonical::formatter::test::ascii_control_characters ... ok
test canonical::formatter::test::ordered_nested_object ... ok
test canonical::formatter::test::securesystemslib_asserts ... ok
test cob::cache::migrations::_2::tests::test_patch_json_deserialization ... ok
test cob::common::test::test_color ... ok
test cob::cache::tests::test_check_version ... ok
test cob::cache::tests::test_migrate_to ... ok
test cob::common::test::test_emojis ... ok
test cob::common::test::test_title ... ok
test cob::cache::migrations::_2::tests::test_migration_2 ... ok
test cob::identity::test::accepted_sibling_causes_rejection ... ok
test cob::identity::test::authorization_based_on_parent_not_current ... ok
test cob::identity::test::cannot_redact_previously_accepted_revision ... ok
test cob::identity::test::accept_rejects_sibling_accept ... ok
test cob::identity::test::concurrent_accept_before_redact_converge ... ok
test cob::identity::test::concurrent_redact_and_accept_converge ... ok
test cob::identity::test::evaluates_queued_children ... ok
test cob::identity::test::cascading_rejections ... ok
test cob::identity::test::has_accepted_active_sibling_returns_true_when_sibling_accepted ... ok
test cob::identity::test::prop_json_eq_str ... ok
test cob::identity::test::evaluation_skips_sibling_accept_in_old_history ... ok
test cob::identity::test::evaluation_strips_author_accept_for_sibling_creation ... ok
test cob::identity::test::evaluates_queued_children_with_new_delegate ... ok
test cob::identity::test::redact_parent_cascades ... ok
test cob::identity::test::reject_concurrent ... ok
test cob::identity::test::remove_delegate_concurrent ... ok
test cob::identity::test::test_identity_redact_revision ... ok
test cob::identity::test::terminal_states_concurrent ... ok
test cob::identity::test::test_identity_update_rejected ... ok
test cob::identity::test::test_identity_updates ... ok
test cob::identity::test::test_identity_cannot_redact_terminal_states ... ok
test cob::identity::test::test_valid_identity ... ok
test cob::identity::test::test_identity_updates_concurrent ... ok
test cob::issue::cache::tests::test_counts ... ok
test cob::issue::cache::tests::test_get ... ok
test cob::issue::cache::tests::test_is_empty ... ok
test cob::issue::cache::tests::test_list ... ok
test cob::issue::cache::tests::test_list_by_status ... ok
test cob::identity::test::test_identity_updates_concurrent_outdated ... ok
test cob::identity::test::update_rejects_second_sibling_proposal ... ok
test cob::issue::cache::tests::test_remove ... ok
test cob::issue::test::test_embeds ... ok
test cob::issue::test::test_embeds_edit ... ok
test cob::issue::test::test_invalid_actions ... ok
test cob::issue::test::test_invalid_cob ... ok
test cob::issue::test::test_invalid_tx ... ok
test cob::issue::test::test_invalid_tx_reference ... ok
test cob::issue::test::test_concurrency ... ok
test cob::issue::test::test_issue_all ... ok
test cob::issue::test::test_issue_comment ... ok
test cob::issue::test::test_issue_create_and_assign ... ok
test cob::issue::test::test_issue_comment_redact ... ok
test cob::issue::test::test_issue_create_and_change_state ... ok
test cob::issue::test::test_issue_create_and_get ... ok
test cob::issue::test::test_issue_create_and_reassign ... ok
test cob::issue::test::test_issue_create_and_unassign ... ok
test cob::issue::test::test_issue_edit ... ok
test cob::issue::test::test_issue_edit_description ... ok
test cob::issue::test::test_issue_multilines ... ok
test cob::issue::test::test_issue_label ... ok
test cob::issue::test::test_issue_state_serde ... ok
test cob::issue::test::test_ordering ... ok
test cob::patch::actions::test::test_review_edit ... ok
test cob::issue::test::test_issue_react ... ok
test cob::issue::test::test_issue_reply ... ok
test cob::patch::cache::tests::test_counts ... ok
test cob::patch::cache::tests::test_is_empty ... ok
test cob::patch::cache::tests::test_list ... ok
test cob::patch::cache::tests::test_list_by_status ... ok
test cob::patch::cache::tests::test_remove ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_migration_null_summary ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_migration_with_summary ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_migration_without_summary ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_v2 ... ok
test cob::patch::encoding::review::test::test_review_summary ... ok
test cob::patch::test::test_json ... ok
test cob::patch::test::test_json_serialisation_target ... ok
test cob::patch::test::test_json_serialization ... ok
test cob::patch::test::test_merge_target_resolution ... ok
test cob::patch::cache::tests::test_get ... ok
test cob::patch::test::test_patch_create_and_get ... ok
test cob::patch::test::test_patch_discussion ... ok
test cob::patch::test::test_patch_merge_authorization_ref_formats ... ok
test cob::patch::test::test_patch_merge_custom_destination_authorized ... ok
test cob::patch::test::test_patch_merge_custom_destination_unauthorized ... ok
test cob::patch::test::test_patch_merge ... ok
test cob::patch::test::test_patch_redact ... ok
test cob::patch::test::test_patch_review ... ok
test cob::patch::test::test_patch_review_comment ... ok
test cob::patch::test::test_patch_review_duplicate ... ok
test cob::patch::test::test_patch_review_edit ... ok
test cob::patch::test::test_patch_review_edit_comment ... ok
test cob::patch::test::test_patch_review_remove_summary ... ok
test cob::patch::test::test_patch_review_revision_redact ... ok
test cob::patch::test::test_reactions_json_serialization ... ok
test cob::patch::test::test_revision_edit_redact ... ok
test cob::patch::test::test_revision_reaction ... ok
test cob::patch::test::test_revision_review_merge_redacted ... ok
test cob::patch::test::test_target_branch ... ok
test cob::stream::tests::test_all_from ... ok
test cob::stream::tests::test_all_from_until ... ok
test cob::stream::tests::test_all_until ... ok
test cob::stream::tests::test_from_until ... ok
test cob::patch::test::test_patch_update ... ok
test cob::stream::tests::test_regression_from_until ... ok
test cob::thread::tests::test_comment_edit_missing ... ok
test cob::thread::tests::test_comment_edit_redacted ... ok
test cob::thread::tests::test_comment_redact_missing ... ok
test cob::thread::tests::test_duplicate_comments ... ok
test cob::thread::tests::test_edit_comment ... ok
test cob::thread::tests::test_redact_comment ... ok
test cob::thread::tests::test_timeline ... ok
test git::canonical::protect::tests::refs_rad ... ok
test git::canonical::protect::tests::refs_rad_id ... ok
test git::canonical::protect::tests::refs_radieschen ... ok
test git::canonical::quorum::test::merge_base_commutative ... ok
test git::canonical::quorum::test::test_merge_bases ... ok
test git::canonical::rules::test::canonical ... ok
test git::canonical::rules::test::deserialization ... ok
test git::canonical::rules::test::deserialize_extensions ... ok
test git::canonical::rules::test::matches_exactly_curly_braces ... ok
test git::canonical::rules::test::matches_expands_globs_appropriately ... ok
test git::canonical::rules::test::ordering ... ok
test git::canonical::rules::test::property::identity ... ok
test git::canonical::rules::test::property::prefix ... ok
test git::canonical::rules::test::property::prefix_negative ... ok
test cob::thread::tests::prop_ordering ... ok
test git::canonical::rules::test::property::suffix ... ok
test git::canonical::rules::test::property::suffix_negative ... ok
test git::canonical::rules::test::roundtrip ... ok
test git::canonical::rules::test::property::trailing_asterisk_partial_component ... ok
test git::canonical::rules::test::rule_validate_success ... ok
test git::canonical::rules::test::special_branches ... ok
test git::canonical::symbolic::test::deserialize_infinite ... ok
test git::canonical::symbolic::test::deserialize_order ... ok
test git::canonical::symbolic::test::deserialize_valid ... ok
test git::canonical::symbolic::test::infinite_extend ... ok
test git::canonical::symbolic::test::infinite_multi ... ok
test git::canonical::symbolic::test::infinite_single ... ok
test git::canonical::symbolic::test::reclassification_combine ... ok
test git::canonical::symbolic::test::reclassification_combine_reverse ... ok
test git::canonical::symbolic::test::reclassification_diamond ... ok
test git::canonical::symbolic::test::reclassification_order_invariant ... ok
test git::canonical::symbolic::test::reclassification_reverse_chain ... ok
test git::canonical::symbolic::test::resolve_two_hop_chain ... ok
test git::canonical::symbolic::test::target_classification ... ok
test git::canonical::symbolic::test::target_classification_symbolic ... ok
test git::canonical::symbolic::test::target_reclassification ... ok
test git::canonical::symbolic::test::target_reclassification_commutative ... ok
test git::canonical::tests::test_commit_quorum_fork_of_a_fork ... ok
test git::canonical::tests::test_commit_quorum_forked_merge_commits ... ok
test git::canonical::tests::test_commit_quorum_groups ... ok
test git::canonical::tests::test_commit_quorum_linear ... ok
test git::canonical::tests::test_commit_quorum_merges ... ok
test git::canonical::tests::test_commit_quorum_single ... ok
test git::canonical::tests::test_commit_quorum_three_way_fork ... ok
test git::canonical::tests::test_commit_quorum_two_way_fork ... ok
test git::canonical::rules::test::rule_validate_failures ... ok
test git::canonical::tests::test_quorum_different_types ... ok
test git::canonical::tests::test_tag_quorum ... ok
test git::test::test_version_from_str ... ok
test git::test::test_version_ord ... ok
test identity::crefs::tests::invalid_clash ... ok
test identity::crefs::tests::invalid_clash_asterisk_name ... ok
test identity::crefs::tests::invalid_dangling ... ok
test identity::crefs::tests::omit_symbolic ... ok
test identity::crefs::tests::valid ... ok
test identity::crefs::tests::valid_asterisk_target ... ok
test identity::did::test::test_did_encode_decode ... ok
test identity::did::test::test_did_vectors ... ok
test identity::doc::test::default_branch_clash ... ok
test identity::doc::test::default_branch_without_project ... ok
test identity::doc::test::prop_encode_decode ... ok
test git::canonical::tests::test_quorum_properties ... ok
test identity::doc::test::test_canonical_doc ... ok
test identity::doc::test::test_duplicate_dids ... ok
test identity::doc::test::test_future_version_error ... ok
test identity::doc::test::test_is_valid_version ... ok
test identity::doc::test::test_max_delegates ... ok
test identity::doc::test::test_canonical_example ... ok
test identity::doc::test::test_parse_version ... ok
test identity::doc::test::test_visibility_json ... ok
test identity::doc::update::test::test_can_update_crefs ... ok
test identity::doc::update::test::test_cannot_include_default_branch_rule ... ok
test identity::doc::update::test::test_default_branch_rule_exists_after_verification ... ok
test identity::project::test::test_project_name ... ok
test identity::doc::test::test_not_found ... ok
test node::address::store::test::skip_invalid_address_type ... ok
test node::address::store::test::skip_mismatched_address_type ... ok
test node::address::store::test::test_alias ... ok
test node::address::store::test::test_disconnected ... ok
test node::address::store::test::test_disconnected_ban ... ok
test node::address::store::test::test_entries ... ok
test node::address::store::test::test_entries_skips_unparsable_address ... ok
test node::address::store::test::test_get_none ... ok
test node::address::store::test::test_insert_and_get ... ok
test node::address::store::test::test_insert_and_remove ... ok
test node::address::store::test::test_insert_and_update ... ok
test node::address::store::test::test_insert_duplicate ... ok
test node::address::store::test::test_node_aliases ... ok
test node::address::store::test::test_remove_nothing ... ok
test node::command::test::command_result ... ok
test node::config::test::deserialize_migrating_scope ... ok
test node::config::test::fetch_level_min ... ok
test node::config::test::onion_absent ... ok
test node::config::test::onion_null ... ok
test node::config::test::partial ... ok
test node::config::test::regression_ipv6_address_brackets ... ok
test node::config::test::regression_ipv6_address_no_brackets ... ok
test node::config::test::serialize_migrating_scope ... ok
test node::config::test::user_agent_custom ... ok
test node::config::test::user_agent_default ... ok
test node::config::test::user_agent_default_explicit ... ok
test node::config::test::user_agent_opt_out ... ok
test node::db::config::test::database_config_valid_combinations ... ok
test node::db::config::test::invalid ... ok
test node::db::test::migration_8::all_ipv6_formatted_dns_addresses_are_retyped ... ok
test node::db::test::migration_8::dns_address_starting_with_bracket_but_missing_closing_bracket_colon_is_unaffected ... ok
test node::db::test::migration_8::dns_address_with_bracket_not_at_start_is_unaffected ... ok
test node::db::test::migration_8::ipv4_address_is_unaffected ... ok
test node::db::test::migration_8::ipv6_formatted_dns_address_is_deleted_when_correct_ipv6_row_already_exists ... ok
test node::db::test::migration_8::ipv6_formatted_dns_address_is_retyped_to_ipv6 ... ok
test node::db::test::migration_8::migration_applies_to_all_nodes ... ok
test node::db::test::migration_8::plain_dns_hostname_without_brackets_is_unaffected ... ok
test node::db::test::migration_8::retype_preserves_address_metadata ... ok
test node::db::test::migration_9::bracketed_non_ipv6_garbage_is_deleted ... ok
test node::db::test::migration_9::dns_row_is_unaffected_even_when_inner_part_has_no_colon ... ok
test node::db::test::migration_9::empty_brackets_ipv6_row_is_deleted ... ok
test node::db::test::migration_9::full_ipv6_address_is_kept ... ok
test node::db::test::migration_9::ipv4_row_is_unaffected ... ok
test node::db::test::migration_9::loopback_address_is_kept ... ok
test node::db::test::migration_9::unspecified_address_is_kept ... ok
test node::db::test::test_version ... ok
test node::features::test::test_operations ... ok
test node::notifications::store::test::test_branch_notifications ... ok
test node::notifications::store::test::test_clear ... ok
test node::notifications::store::test::test_cob_notifications ... ok
test node::notifications::store::test::test_counts_by_repo ... ok
test node::notifications::store::test::test_duplicate_notifications ... ok
test node::notifications::store::test::test_notification_status ... ok
test node::policy::store::test::test_follow_and_unfollow_node ... ok
test node::policy::store::test::test_node_aliases ... ok
test node::policy::store::test::test_node_policies ... ok
test node::policy::store::test::test_node_policy ... ok
test node::policy::store::test::test_repo_policies ... ok
test node::policy::store::test::test_repo_policy ... ok
test node::policy::store::test::test_seed_and_unseed_repo ... ok
test node::policy::store::test::test_update_alias ... ok
test node::policy::store::test::test_update_scope ... ok
test node::refs::store::test::test_count ... ok
test node::refs::store::test::test_set_and_delete ... ok
test node::refs::store::test::test_set_and_get ... ok
test node::routing::test::test_count ... ok
test node::routing::test::test_entries ... ok
test node::routing::test::test_insert_and_get ... ok
test node::routing::test::test_insert_and_get_resources ... ok
test node::routing::test::test_insert_and_remove ... ok
test node::routing::test::test_insert_duplicate ... ok
test node::routing::test::test_insert_existing_updated_time ... ok
test node::routing::test::test_len ... ok
test node::address::store::test::test_empty ... ok
test node::routing::test::test_prune ... ok
test node::routing::test::test_remove_many ... ok
test node::routing::test::test_update_existing_multi ... ok
test node::sync::announce::test::all_synced_nodes_are_preferred_seeds ... ok
test node::sync::announce::test::announcer_adapts_target_to_reach ... ok
test node::sync::announce::test::announcer_preferred_seeds_or_replica_factor ... ok
test node::routing::test::test_remove_redundant ... ok
test node::sync::announce::test::announcer_reached_max_replication_target ... ok
test node::sync::announce::test::announcer_reached_min_replication_target ... ok
test node::sync::announce::test::announcer_reached_preferred_seeds ... ok
test node::sync::announce::test::announcer_synced_with_unknown_node ... ok
test node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds ... ok
test node::sync::announce::test::announcer_timed_out ... ok
test node::sync::announce::test::cannot_construct_announcer ... ok
test node::sync::announce::test::construct_node_appears_in_multiple_input_sets ... ok
test node::sync::announce::test::construct_only_preferred_seeds_provided ... ok
test node::sync::announce::test::invariant_progress_should_match_state ... ok
test node::sync::announce::test::local_node_in_multiple_sets ... ok
test node::sync::announce::test::local_node_in_preferred_seeds ... ok
test node::sync::announce::test::local_node_in_synced_set ... ok
test node::sync::announce::test::local_node_only_in_all_sets_results_in_no_seeds_error ... ok
test node::sync::announce::test::local_node_in_unsynced_set ... ok
test node::sync::announce::test::preferred_seeds_already_synced ... ok
test node::sync::announce::test::synced_with_local_node_is_ignored ... ok
test node::sync::announce::test::synced_with_same_node_multiple_times ... ok
test node::sync::announce::test::timed_out_after_reaching_success ... ok
test node::sync::fetch::test::all_nodes_are_candidates ... ok
test node::sync::fetch::test::all_nodes_are_fetchable ... ok
test node::sync::fetch::test::ignores_duplicates_and_local_node ... ok
test node::sync::fetch::test::could_not_reach_target ... ok
test node::sync::fetch::test::preferred_seeds_target_returned_over_replicas ... ok
test node::sync::fetch::test::reaches_target_of_max_replicas ... ok
test node::sync::fetch::test::reaches_target_of_preferred_seeds ... ok
test node::sync::fetch::test::reaches_target_of_replicas ... ok
test node::sync::test::ensure_replicas_construction ... ok
test node::sync::test::replicas_constrain_to ... ok
test node::test::test_address ... ok
test node::test::test_alias ... ok
test node::test::test_command_result ... ok
test node::test::test_user_agent ... ok
test node::timestamp::tests::test_timestamp_max ... ok
test profile::test::canonicalize_home ... ok
test profile::test::test_config ... ok
test rad::tests::test_checkout ... ok
test rad::tests::test_fork ... ok
test rad::tests::test_init ... ok
test profile::config::test::schema ... ok
test storage::git::tests::test_references_of ... ok
test storage::git::tests::test_sign_refs ... ok
test storage::git::transport::local::url::test::test_url_parse ... ok
test storage::git::transport::remote::url::test::test_url_parse ... ok
test storage::git::transport::local::url::test::test_url_to_string ... ok
test storage::refs::sigrefs::git::properties::idempotent_write ... ok
test cob::identity::test::property::prop_invariants ... ok
test cob::patch::cache::tests::test_find_by_revision ... ok
test storage::refs::sigrefs::git::properties::initial_commit_roundtrip ... ok
test storage::refs::sigrefs::read::test::commit_reader::identity_root_error ... ok
test storage::refs::sigrefs::read::test::commit_reader::missing_commit ... ok
test storage::refs::sigrefs::read::test::commit_reader::read_ok ... ok
test storage::refs::sigrefs::read::test::commit_reader::too_many_parents ... ok
test storage::refs::sigrefs::read::test::commit_reader::tree_error ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::doc_blob_error ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::missing_identity ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::read_ok_none ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::read_ok_some ... ok
test storage::refs::sigrefs::read::test::resolve_tip::find_reference_error ... ok
test storage::refs::sigrefs::read::test::resolve_tip::missing_sigrefs ... ok
test storage::refs::sigrefs::read::test::resolve_tip::resolve_tip_ok ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::detect_parent::root_without_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::detect_parent::root_without_root ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::restore ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::root ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::root_with_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::head_commit_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::head_verify_mismatched_identity_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::head_verify_signature_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::invalid_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::read_ok_no_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::read_ok_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::read_ok_root ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::alternating ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::chain ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::multiple ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::root_at_head ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::single_commit ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::two_commits ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::walk_commit_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::walk_verify_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::missing_both ... ok
test storage::refs::sigrefs::read::test::tree_reader::missing_refs ... ok
test storage::refs::sigrefs::read::test::tree_reader::missing_signature ... ok
test storage::refs::sigrefs::read::test::tree_reader::parse_refs_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::parse_signature_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::read_ok ... ok
test storage::refs::sigrefs::read::test::tree_reader::read_refs_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::read_signature_error ... ok
test storage::refs::sigrefs::write::test::commit_writer::tree_error ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_commit_error ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_empty_refs ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_root_ok ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_with_parent_ok ... ok
test storage::refs::sigrefs::write::test::head_reader::no_head ... ok
test storage::refs::sigrefs::write::test::head_reader::read_ok ... ok
test storage::refs::sigrefs::write::test::head_reader::reference_error ... ok
test storage::refs::sigrefs::write::test::head_reader::refs_blob_error ... ok
test storage::refs::sigrefs::write::test::head_reader::refs_blob_missing ... ok
test storage::refs::sigrefs::write::test::head_reader::refs_parse_error ... ok
test storage::refs::sigrefs::write::test::head_reader::signature_blob_error ... ok
test storage::refs::sigrefs::write::test::head_reader::signature_blob_missing ... ok
test storage::refs::sigrefs::write::test::head_reader::signature_parse_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::commit_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::head_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::never_write_rad_sigrefs ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::reference_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::unchanged ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::unchanged_force_writes_new_commit ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::write_empty_refs ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::write_root_ok ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::write_with_parent_ok ... ok
test storage::refs::sigrefs::write::test::tree_writer::sign_error ... ok
test storage::refs::sigrefs::write::test::tree_writer::write_ok ... ok
test storage::refs::sigrefs::write::test::tree_writer::write_tree_error ... ok
test storage::refs::tests::prop_canonical_roundtrip ... ok
test storage::refs::tests::test_rid_verification ... ok
test storage::refs::sigrefs::property::idempotent ... ok
test storage::tests::test_storage ... ok
test test::assert::test::assert_with_message ... ok
test test::assert::test::test_assert_no_move ... ok
test test::assert::test::test_assert_panic_0 - should panic ... ok
test test::assert::test::test_assert_panic_1 - should panic ... ok
test test::assert::test::test_assert_panic_2 - should panic ... ok
test test::assert::test::test_assert_succeed ... ok
test version::test::test_version ... ok
test web::test::description_only ... ok
test web::test::pinned_empty ... ok
test test::assert::test::test_panic_message ... ok
test storage::refs::sigrefs::git::properties::chain_roundtrip ... ok
test storage::refs::sigrefs::property::roundtrip ... ok
test result: ok. 400 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.60s
Running unittests src/lib.rs (target/debug/deps/radicle_cli-a5bcfd81341416bb)
running 46 tests
test commands::block::args::test::should_parse_rid ... ok
test commands::block::args::test::should_not_parse ... ok
test commands::block::args::test::should_parse_nid ... ok
test commands::clone::args::test::should_parse_rid_non_urn ... ok
test commands::clone::args::test::should_parse_rid_url ... ok
test commands::clone::args::test::should_parse_rid_urn ... ok
test commands::cob::args::test::should_allow_log_json_format ... ok
test commands::cob::args::test::should_allow_log_pretty_format ... ok
test commands::cob::args::test::should_allow_show_json_format ... ok
test commands::cob::args::test::should_not_allow_show_pretty_format ... ok
test commands::cob::args::test::should_not_allow_update_pretty_format ... ok
test commands::cob::args::test::should_allow_update_json_format ... ok
test commands::fork::args::test::should_not_parse_rid_url ... ok
test commands::fork::args::test::should_parse_rid_non_urn ... ok
test commands::fork::args::test::should_parse_rid_urn ... ok
test commands::id::args::test::should_not_clobber_payload_args ... ok
test commands::id::args::test::should_not_parse_into_payload - should panic ... ok
test commands::id::args::test::should_not_parse_single_payload ... ok
test commands::id::args::test::should_parse_into_payload ... ok
test commands::id::args::test::should_not_parse_single_payloads ... ok
test commands::id::args::test::should_parse_multiple_payloads ... ok
test commands::init::args::test::should_not_parse_rid_url ... ok
test commands::init::args::test::should_parse_rid_non_urn ... ok
test commands::id::args::test::should_parse_single_payload ... ok
test commands::inspect::test::test_tree ... ok
test commands::init::args::test::should_parse_rid_urn ... ok
test commands::patch::review::builder::tests::test_review_comments_basic ... ok
test commands::patch::review::builder::tests::test_review_comments_before ... ok
test commands::patch::review::builder::tests::test_review_comments_multiline ... ok
test commands::patch::review::builder::tests::test_review_comments_split_hunk ... ok
test commands::publish::args::test::should_not_parse_rid_url ... ok
test commands::publish::args::test::should_parse_rid_non_urn ... ok
test commands::publish::args::test::should_parse_rid_urn ... ok
test git::pretty_diff::test::test_pretty ... ignored
test git::ddiff::tests::diff_encode_decode_ddiff_hunk ... ok
test git::unified_diff::test::test_diff_content_encode_decode_content ... ok
test git::unified_diff::test::test_diff_encode_decode_diff ... ok
test terminal::args::test::should_not_parse ... ok
test commands::watch::args::test::should_parse_ref_str ... ok
test terminal::args::test::should_parse_nid ... ok
test terminal::args::test::should_parse_rid ... ok
test terminal::format::test::test_bytes ... ok
test terminal::format::test::test_strip_comments ... ok
test terminal::patch::test::test_edit_display_message ... ok
test terminal::patch::test::test_create_display_message ... ok
test terminal::patch::test::test_update_display_message ... ok
test result: ok. 45 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/main.rs (target/debug/deps/rad-e50ea6c281bfadf2)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/commands.rs (target/debug/deps/commands-310baf170bf13c5a)
running 126 tests
test commands::checkout::rad_checkout ... ok
test commands::clone::rad_clone_bare ... ok
test commands::clone::rad_clone ... ok
test commands::clone::rad_clone_all ... ok
test commands::clone::rad_clone_scope ... ok
test commands::clone::rad_clone_directory ... ok
test commands::clone::rad_clone_unknown ... ok
test commands::clone::rad_clone_partial_fail ... ok
test commands::clone::rad_clone_connect ... ok
test commands::cob::rad_cob_multiset ... ok
test commands::clone::test_clone_without_seeds ... ok
test commands::cob::rad_cob_log ... ok
test commands::cob::rad_cob_operations ... ok
test commands::cob::rad_cob_migrate ... ok
test commands::cob::rad_cob_show ... ok
test commands::cob::rad_cob_update_identity ... ok
test commands::cob::rad_cob_update ... ok
test commands::cob::test_cob_deletion ... ok
test commands::cob::test_cob_replication ... ok
test commands::git::git_push_amend ... ok
test commands::git::git_push_and_fetch ... ok
test commands::git::git_push_canonical_lightweight_tags ... ok
test commands::git::git_push_diverge ... ok
test commands::git::git_push_force_with_lease ... ok
test commands::git::git_push_canonical ... ok
test commands::id::rad_id ... ok
test commands::id::rad_id_collaboration ... ignored, slow
test commands::git::git_push_converge ... ok
test commands::git::git_push_rollback ... ok
test commands::git::git_tag ... ok
test commands::id::rad_id_private ... ok
test commands::id::rad_id_threshold_soft_fork ... ok
test commands::id::rad_id_conflict ... ok
test commands::id::rad_id_threshold ... ok
test commands::id::rad_id_unknown_field ... ok
test commands::id::rad_id_unauthorized_delegate ... ok
test commands::init::rad_init ... ignored, part of many other tests
test commands::id::rad_id_update_delete_field ... ok
test commands::id::rad_id_multi_delegate ... ok
test commands::init::rad_init_detached_head ... ok
test commands::init::rad_init_existing ... ok
test commands::init::rad_init_bare ... ok
test commands::init::rad_init_existing_bare ... ok
test commands::init::rad_init_no_git ... ok
test commands::init::rad_init_no_seed ... ok
test commands::init::rad_init_private ... ok
test commands::init::rad_init_private_no_seed ... ok
test commands::init::rad_init_private_clone ... ok
test commands::inbox::rad_inbox ... ok
test commands::init::rad_init_private_clone_seed ... ok
test commands::init::rad_init_private_seed ... ok
test commands::init::rad_init_sync_not_connected ... ok
test commands::init::rad_init_sync_preferred ... ok
test commands::init::rad_init_with_existing_remote ... ok
test commands::init::rad_publish ... ok
test commands::issue::rad_issue ... ok
test commands::jj::rad_jj_bare ... ignored, the bare repository does not have a `rad` remote, and so it cannot determine the RID of the repository
test commands::jj::rad_jj_colocated_patch ... ok
test commands::issue::rad_issue_list ... ok
test commands::node::rad_node_connect ... ok
test commands::node::rad_node_connect_without_address ... ok
test commands::patch::rad_merge_after_update ... ok
test commands::patch::rad_merge_no_ff ... ok
test commands::node::rad_node ... ok
test commands::patch::rad_merge_via_push ... ok
test commands::patch::rad_patch ... ok
test commands::patch::rad_patch_ahead_behind ... ok
test commands::patch::rad_patch_change_base ... ok
test commands::patch::rad_patch_checkout ... ok
test commands::patch::rad_patch_checkout_revision ... ok
test commands::init::rad_init_sync_timeout ... ok
test commands::init::rad_init_sync_and_clone ... ok
test commands::patch::rad_patch_checkout_force ... ok
test commands::patch::rad_patch_detached_head ... ok
test commands::patch::rad_patch_draft ... ok
test commands::patch::rad_patch_diff ... ok
test commands::patch::rad_patch_edit ... ok
test commands::patch::rad_patch_fetch_2 ... ok
test commands::patch::rad_patch_merge_draft ... ok
test commands::patch::rad_patch_delete ... ok
test commands::patch::rad_patch_fetch_1 ... ok
test commands::patch::rad_patch_magic_push ... ok
test commands::patch::rad_patch_merge_into_canonical_ref_branch ... ok
test commands::patch::rad_patch_merge_wrong_branch ... ok
test commands::patch::rad_patch_merge_on_first_push ... ok
test commands::patch::rad_patch_merge_unauthorized_branch ... ok
test commands::patch::rad_patch_open_explore ... ok
test commands::patch::rad_patch_revert_merge ... ok
test commands::patch::rad_patch_revert_custom_branch ... ok
test commands::patch::rad_patch_update ... ok
test commands::patch::rad_patch_review_no_options ... ok
test commands::patch::rad_patch_via_push ... ok
test commands::patch::rad_review_by_hunk ... ok
test commands::policy::rad_block ... ok
test commands::policy::rad_seed_and_follow ... ok
test commands::policy::rad_seed_policy_allow_no_scope ... ok
test commands::policy::rad_seed_scope ... ok
test commands::policy::rad_unseed ... ok
test commands::policy::rad_seed_many ... ok
test commands::policy::rad_unseed_many ... ok
test commands::sigpipe::config ... ok
test commands::sigpipe::help ... ok
test commands::sigpipe::rad_self ... ok
test commands::patch::rad_push_and_pull_patches ... ok
test commands::remote::rad_remote ... ok
test commands::sync::rad_sync_without_node ... ok
test commands::sync::rad_sync ... ok
test commands::utility::framework_home ... ok
test commands::utility::rad_auth ... ok
test commands::utility::rad_auth_errors ... ok
test commands::patch::rad_patch_pull_update ... ok
test commands::utility::rad_config ... ok
test commands::utility::rad_diff ... ok
test commands::utility::rad_clean ... ok
test commands::utility::rad_help ... ok
test commands::utility::rad_inspect ... ok
test commands::utility::rad_key_mismatch ... ok
test commands::utility::rad_self ... ok
test commands::utility::rad_warn_ipv6 ... ok
test commands::utility::rad_warn_old_nodes ... ok
test commands::watch::rad_watch ... ok
test commands::sync::rad_fetch ... ok
test rad_remote ... ok
test commands::workflow::rad_workflow ... ok
test commands::sync::test_replication_via_seed ... ok
test commands::utility::rad_fork ... ok
test result: ok. 123 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 73.17s
Running unittests src/lib.rs (target/debug/deps/radicle_cli_test-d74a2f47954f5056)
running 3 tests
test tests::test_parse ... ok
test tests::test_run ... ok
test tests::test_example_spaced_brackets ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_cob-b671f17fefab1c97)
running 9 tests
test object::tests::test_serde ... ok
test tests::git::roundtrip ... ok
test tests::git::list_cobs ... ok
test tests::git::update_cob ... ok
test type_name::test::invalid_typenames ... ok
test type_name::test::valid_typenames ... ok
test tests::parse_refstr ... ok
test tests::git::traverse_cobs ... ok
test tests::invalid_parse_refstr ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/lib.rs (target/debug/deps/radicle_core-354608ffd075d299)
running 4 tests
test repo::test::valid ... ok
test repo::test::invalid ... ok
test repo::test::assert_prop_roundtrip_parse ... ok
test repo::serde_impls::test::assert_prop_roundtrip_serde_json ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_crypto-282b7f21f8aa9307)
running 12 tests
test ssh::agent::test::test_agent_encoding_remove ... ok
test ssh::agent::test::test_agent_encoding_sign ... ok
test ssh::fmt::test::test_fingerprint ... ok
test ssh::fmt::test::test_key ... ok
test tests::did_key_sample ... ok
test ssh::keystore::tests::test_init_no_passphrase ... ok
test tests::diffie_hellman_fixture ... ok
test tests::prop_encode_decode ... ok
test tests::prop_key_equality ... ok
test tests::diffie_hellman ... ok
test ssh::keystore::tests::test_signer ... ok
test ssh::keystore::tests::test_init_passphrase ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s
Running unittests src/lib.rs (target/debug/deps/radicle_dag-cba03ea2f40c887a)
running 20 tests
test tests::test_contains ... ok
test tests::test_cycle ... ok
test tests::test_dependencies ... ok
test tests::test_diamond ... ok
test tests::test_fold_multiple_roots ... ok
test tests::test_fold_diamond ... ok
test tests::test_fold_sorting_1 ... ok
test tests::test_fold_reject ... ok
test tests::test_fold_sorting_2 ... ok
test tests::test_get ... ok
test tests::test_complex ... ok
test tests::test_is_empty ... ok
test tests::test_len ... ok
test tests::test_merge_1 ... ok
test tests::test_merge_2 ... ok
test tests::test_prune_1 ... ok
test tests::test_prune_2 ... ok
test tests::test_prune_by_sorting ... ok
test tests::test_remove ... ok
test tests::test_siblings ... ok
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_fetch-c769c3c9a573d142)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_git_metadata-10d7506669fe8c72)
running 24 tests
test commit::parse::test::error::invalid_author ... ok
test commit::parse::test::error::invalid_committer ... ok
test commit::parse::test::error::invalid_format_continuation_without_preceding_header ... ok
test commit::parse::test::error::invalid_parent ... ok
test commit::parse::test::error::invalid_tree ... ok
test commit::parse::test::error::missing_author ... ok
test commit::parse::test::error::missing_committer ... ok
test commit::parse::test::error::missing_header_body_separator ... ok
test commit::parse::test::error::missing_tree_empty_header ... ok
test commit::parse::test::error::missing_tree_wrong_first_line ... ok
test commit::parse::test::success::commit_gpgsig_is_preserved_and_strip_removes_it ... ok
test commit::parse::test::success::commit_last_paragraph_kept_in_message_when_not_all_trailers ... ok
test commit::parse::test::success::commit_with_extra_headers ... ok
test commit::parse::test::success::commit_with_multiline_gpgsig ... ok
test commit::parse::test::success::commit_with_single_parent ... ok
test commit::parse::test::success::commit_with_trailers ... ok
test commit::parse::test::success::merge_commit ... ok
test commit::parse::test::success::root_commit ... ok
test commit::parse::test::unit::body_last_paragraph_not_trailers_stays_in_message ... ok
test commit::parse::test::success::roundtrip ... ok
test commit::parse::test::unit::trailers_accepts_empty_input ... ok
test commit::parse::test::unit::trailers_rejects_invalid_token_chars ... ok
test commit::parse::test::unit::trailers_rejects_line_without_separator ... ok
test commit::parse::test::unit::body_no_paragraph_separator_means_no_trailers ... ok
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_git_ref_format-53cacdde7021555b)
running 9 tests
test test::component ... ok
test test::component_invalid - should panic ... ok
test test::qualified ... ok
test test::pattern ... ok
test test::qualified_invalid - should panic ... ok
test test::qualified_pattern_invalid - should panic ... ok
test test::qualified_pattern ... ok
test test::refname ... ok
test test::refname_invalid - should panic ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_localtime-834cec7348dd66f7)
running 1 test
test serde_impls::test::test_localtime ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_log-c7a44cc83fe87a10)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_node-7c4ecb3547a7fb7b)
running 84 tests
test reactor::timer::tests::test_next ... ok
test reactor::timer::tests::test_wake ... ok
test reactor::timer::tests::test_wake_exact ... ok
test control::tests::test_control_socket ... ok
test fingerprint::tests::matching ... ok
test control::tests::test_seed_unseed ... ok
test tests::announcement_rebroadcast_timestamp_filtered ... ok
test tests::announcement_rebroadcast ... ok
test tests::announcement_rebroadcast_duplicates ... ok
test tests::connection_kept_alive ... ok
test tests::disconnecting_unresponsive_peer ... ok
test tests::announcement_relay ... ok
test tests::e2e::fetch_does_not_contain_rad_sigrefs_parent ... ok
test tests::e2e::missing_default_branch ... ok
test tests::e2e::missing_delegate_default_branch ... ok
test tests::e2e::test_block_active_connection ... ok
test tests::e2e::test_block_prevents_connection ... ok
test tests::e2e::test_block_prevents_fetch ... ok
test tests::e2e::test_background_foreground_fetch ... ok
test tests::e2e::test_catchup_on_refs_announcements ... ok
test tests::e2e::test_channel_reader_limit ... ok
test tests::e2e::test_connection_crossing ... ok
test tests::e2e::test_clone ... ok
test tests::e2e::test_dont_fetch_owned_refs ... ok
test tests::e2e::test_fetch_emits_canonical_ref_update_partial_glob ... ok
test tests::e2e::test_concurrent_fetches ... ok
test tests::e2e::test_fetch_followed_remotes ... ok
test tests::e2e::test_fetch_preserve_owned_refs ... ok
test tests::announcement_message_amplification ... ok
test tests::e2e::test_fetch_unseeded ... ok
test tests::e2e::test_fetch_up_to_date ... ok
test tests::e2e::test_fetch_emits_canonical_ref_update ... ok
test tests::e2e::test_inventory_sync_basic ... ok
test tests::e2e::test_large_fetch ... ok
test tests::e2e::test_migrated_clone ... ok
test tests::e2e::test_missing_remote ... ok
test tests::e2e::test_multiple_offline_inits ... ok
test tests::e2e::test_non_fast_forward_identity_doc ... ok
test tests::e2e::test_non_fast_forward_sigrefs ... ok
test tests::e2e::test_outdated_delegate_sigrefs ... ok
test tests::e2e::test_outdated_sigrefs ... ok
test tests::e2e::test_replication ... ok
test tests::e2e::test_replication_invalid ... ok
test tests::e2e::test_inventory_sync_bridge ... ok
test tests::fetch_missing_inventory_on_gossip ... ok
test tests::fetch_missing_inventory_on_schedule ... ok
test tests::e2e::test_replication_ref_in_sigrefs ... ok
test tests::inbound_connection ... ok
test tests::inventory_decode ... ok
test tests::init_and_seed ... ok
test tests::e2e::test_inventory_sync_ring ... ok
test tests::e2e::test_inventory_sync_star ... ok
test tests::inventory_relay_bad_timestamp ... ok
test tests::inventory_relay ... ok
test tests::inventory_sync ... ok
test tests::maintain_connections_failed_attempt ... ok
test tests::maintain_connections_same_second_loop ... ok
test tests::maintain_connections_transient ... ok
test tests::maintain_connections ... ok
test tests::orphaned_fetch_blocks_repo_from_all_nodes ... ok
test tests::outbound_connection ... ok
test tests::persistent_peer_connect ... ok
test tests::persistent_peer_reconnect_attempt ... ok
test tests::persistent_peer_reconnect_success ... ok
test tests::ping_response ... ok
test tests::queued_fetch_from_ann_same_rid ... ok
test tests::queued_fetch_from_command_same_rid ... ok
test tests::queued_fetch_max_capacity ... ok
test tests::redundant_connect ... ok
test tests::refs_announcement_fetch_trusted_no_inventory ... ok
test tests::refs_announcement_followed ... ok
test tests::inventory_pruning ... ok
test tests::refs_announcement_no_subscribe ... ok
test tests::refs_announcement_offline ... ok
test tests::refs_announcement_relay_private ... ok
test tests::refs_announcement_relay_public ... ok
test tests::refs_synced_event ... ok
test wire::test::test_inventory_ann_with_extension ... ok
test wire::test::test_pong_message_with_extension ... ok
test tests::seeding ... ok
test tests::seed_repo_subscribe ... ok
test wire::test::worker_result_clears_active_when_peer_connected ... ok
test wire::test::worker_result_clears_active_when_peer_disconnecting ... ok
test tests::prop_inventory_exchange_dense ... ok
test result: ok. 84 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.02s
Running unittests src/main.rs (target/debug/deps/radicle_node-e74da89847c715ea)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_oid-edf5ff98ab5a168a)
running 10 tests
test fmt::test::fixture ... ok
test git2::test::zero ... ok
test gix::test::zero ... ok
test fmt::test::zero ... ok
test fmt::test::git2 ... ok
test fmt::test::gix ... ok
test str::test::fixture ... ok
test str::test::zero ... ok
test str::test::gix_roundtrip ... ok
test str::test::git2_roundtrip ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_protocol-a3c229b4119e3799)
running 100 tests
test deserializer::test::test_decode_next ... ok
test deserializer::test::test_unparsed ... ok
test deserializer::test::prop_decode_next ... ok
test fetcher::service::tests::test_fetch_coalescing_different_refs ... ok
test fetcher::test::queue::properties::capacity::capacity_reached_returns_same_item ... ok
test fetcher::test::queue::properties::dequeue::drained_queue_returns_none ... ok
test fetcher::test::queue::properties::dequeue::empty_queue_returns_none ... ok
test fetcher::test::queue::properties::dequeue::enables_reenqueue ... ok
test fetcher::test::queue::properties::capacity::rejection ... ok
test fetcher::test::queue::properties::capacity::bounded ... ok
test fetcher::test::queue::properties::equality::reflexive ... ok
test fetcher::test::queue::properties::fifo::interleaved_operations ... ok
test fetcher::test::queue::properties::fifo::ordering ... ok
test fetcher::test::queue::properties::equality::symmetric ... ok
test fetcher::test::queue::properties::equality::transitive ... ok
test fetcher::test::queue::properties::merge::different_rid_accepted ... ok
test fetcher::test::queue::properties::merge::combines_refs ... ok
test fetcher::test::queue::properties::merge::longer_timeout_preserved ... ok
test fetcher::test::queue::properties::merge::does_not_increase_queue_length ... ok
test fetcher::test::queue::properties::merge::empty_refs_fetches_all ... ok
test fetcher::test::queue::unit::capacity_takes_precedence_over_merge_for_new_items ... ok
test fetcher::test::queue::unit::empty_refs_items_can_be_equal ... ok
test fetcher::test::queue::properties::capacity::restored_after_dequeue ... ok
test fetcher::test::queue::unit::max_timeout_accepted ... ok
test fetcher::test::queue::unit::zero_timeout_accepted ... ok
test fetcher::test::queue::unit::merge_preserves_position_in_queue ... ok
test fetcher::test::state::command::cancel::non_existent_returns_unexpected ... ok
test fetcher::test::state::command::cancel::cancellation_is_isolated ... ok
test fetcher::test::queue::properties::merge::succeed_when_at_capacity ... ok
test fetcher::test::state::command::cancel::ongoing_and_queued ... ok
test fetcher::test::state::command::cancel::single_ongoing ... ok
test fetcher::test::state::command::fetch::fetch_after_previous_completed ... ok
test fetcher::test::state::command::fetch::fetch_different_repo_same_node_within_capacity ... ok
test fetcher::test::state::command::fetch::fetch_at_capacity_enqueues ... ok
test fetcher::test::state::command::fetch::fetch_duplicate_returns_already_fetching ... ok
test fetcher::test::state::command::fetch::fetch_queue_merge_empty_refs_fetches_all ... ok
test fetcher::test::state::command::fetch::fetch_queue_merge_takes_longer_timeout ... ok
test fetcher::test::state::command::fetch::fetch_same_repo_different_nodes_queues_second ... ok
test fetcher::test::state::command::fetch::fetch_queue_rejected_capacity_reached ... ok
test fetcher::test::state::command::fetch::fetch_start_first_fetch_for_node ... ok
test fetcher::test::state::command::fetch::fetch_same_repo_different_refs_enqueues ... ok
test fetcher::test::state::command::fetch::fetch_queue_merges_already_queued ... ok
test fetcher::test::state::command::fetched::complete_one_of_multiple ... ok
test fetcher::test::state::command::fetched::complete_then_dequeue_fifo ... ok
test fetcher::test::state::command::fetched::non_existent_returns_not_found ... ok
test fetcher::test::state::command::fetched::complete_single_ongoing ... ok
test fetcher::test::state::command::fetched::stale_from_does_not_clear_active ... ok
test fetcher::test::state::concurrent::fetched_then_cancel ... ok
test fetcher::test::state::concurrent::interleaved_operations ... ok
test fetcher::test::state::config::min_queue_size ... ok
test fetcher::test::state::dequeue::cannot_dequeue_while_node_at_capacity ... ok
test fetcher::test::state::dequeue::empty_queue_returns_none ... ok
test fetcher::test::state::config::high_concurrency ... ok
test fetcher::test::state::invariant::queue_integrity_after_merge ... ok
test fetcher::test::state::dequeue::maintains_fifo_order ... ok
test service::filter::test::compatible ... ok
test service::filter::test::test_parameters ... ok
test fetcher::test::state::multinode::independent_queues ... ok
test service::gossip::store::test::test_announced ... ok
test service::limiter::test::test_limiter_different_rates ... ok
test service::limiter::test::test_limiter_multi ... ok
test service::limiter::test::test_limiter_refill ... ok
test service::filter::test::test_sizes ... ok
test fetcher::test::state::multinode::high_count ... ok
test service::message::tests::prop_refs_announcement_signing ... ok
test service::message::tests::test_inventory_limit ... ok
test wire::frame::test::test_encode_git_large ... ok
test wire::frame::test::test_stream_id ... ok
test service::message::tests::test_ref_remote_limit ... ok
test wire::message::tests::prop_roundtrip_address ... ok
test fetcher::test::queue::properties::merge::same_rid_merges_anywhere_in_queue ... ok
test wire::message::tests::prop_zero_bytes_encode_decode ... ok
test wire::message::tests::prop_roundtrip_message ... ok
test wire::message::tests::test_node_ann_max_size ... ok
test wire::message::tests::test_ping_encode_size_overflow - should panic ... ok
test wire::message::tests::test_pingpong_encode_max_size ... ok
test wire::message::tests::test_pong_encode_size_overflow - should panic ... ok
test wire::message::tests::test_inv_ann_max_size ... ok
test wire::tests::prop_oid ... ok
test wire::tests::prop_roundtrip_filter ... ok
test wire::tests::prop_roundtrip_publickey ... ok
test wire::tests::prop_roundtrip_refs ... ok
test wire::tests::prop_roundtrip_repoid ... ok
test wire::tests::prop_roundtrip_tuple ... ok
test wire::tests::prop_roundtrip_u16 ... ok
test wire::tests::prop_roundtrip_u32 ... ok
test wire::tests::prop_roundtrip_u64 ... ok
test wire::tests::prop_roundtrip_vec ... ok
test wire::tests::prop_signature ... ok
test wire::tests::prop_string ... ok
test wire::tests::test_alias ... ok
test wire::tests::test_bounded_vec_limit ... ok
test wire::tests::test_filter_invalid ... ok
test wire::tests::test_string ... ok
test wire::varint::test::prop_roundtrip_varint ... ok
test wire::varint::test::test_encode_overflow - should panic ... ok
test wire::varint::test::test_encoding ... ok
test wire::message::tests::test_refs_ann_max_size ... ok
test service::message::tests::test_node_announcement_validate ... ok
test wire::message::tests::prop_message_decoder ... ok
test result: ok. 100 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s
Running unittests src/main.rs (target/debug/deps/git_remote_rad-8a5fa7fc5e1d915f)
running 12 tests
test protocol::tests::test_empty ... ok
test protocol::tests::test_capabilities ... ok
test protocol::tests::test_fetch_whitespace ... ok
test protocol::tests::test_fetch ... ok
test protocol::tests::test_list ... ok
test protocol::tests::test_invalid ... ok
test protocol::tests::test_option_whitespace_preservation ... ok
test protocol::tests::test_option ... ok
test protocol::tests::test_list_for_push ... ok
test protocol::tests::test_push ... ok
test protocol::tests::test_push_delete ... ok
test protocol::tests::test_push_force ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/radicle_schemars-0c69f7aee46217f9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_signals-3c6a43adea3f1f99)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_systemd-e493431dcbdbcb38)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_term-4725074291246d98)
running 22 tests
test ansi::tests::colors_disabled ... ok
test cell::test::test_width ... ok
test element::test::test_spaced ... ok
test ansi::tests::colors_enabled ... ok
test ansi::tests::no_color_disables_colors_not_styles ... ok
test element::test::test_truncate ... ok
test element::test::test_width ... ok
test ansi::tests::wrapping ... ok
test table::test::test_table ... ok
test table::test::test_table_border_maximized ... ok
test table::test::test_table_border_truncated ... ok
test table::test::test_table_border ... ok
test table::test::test_truncate ... ok
test table::test::test_table_truncate ... ok
test table::test::test_table_unicode ... ok
test table::test::test_table_unicode_truncate ... ok
test textarea::test::test_wrapping_code_block ... ok
test textarea::test::test_wrapping_fenced_block ... ok
test textarea::test::test_wrapping ... ok
test vstack::test::test_vstack ... ok
test vstack::test::test_vstack_maximize ... ok
test textarea::test::test_wrapping_paragraphs ... ok
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_windows-73a4d59102760b64)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle
running 1 test
test crates/radicle/src/cob/patch/encoding/review.rs - cob::patch::encoding::review::Review (line 23) ... ignored
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_cli
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_cli_test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_cob
running 1 test
test crates/radicle-cob/src/backend/stable.rs - backend::stable::with_advanced_timestamp (line 56) ... ignored
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.08s; merged doctests compilation took 0.08s
Doc-tests radicle_core
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_crypto
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_dag
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_fetch
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_git_metadata
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_git_ref_format
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_localtime
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_log
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_node
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_oid
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_protocol
running 6 tests
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::collect_from (line 30) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::push (line 122) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::truncate (line 50) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::max (line 96) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::unbound (line 149) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::with_capacity (line 66) ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.49s; merged doctests compilation took 0.47s
Doc-tests radicle_signals
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_systemd
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_term
running 1 test
test crates/radicle-term/src/table.rs - table (line 4) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.16s; merged doctests compilation took 0.16s
Doc-tests radicle_windows
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Exit code: 0
{
"response": "finished",
"result": "success"
}