diff --git a/Cargo.lock b/Cargo.lock index 9ded68ac..7ab33f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,6 +1092,7 @@ dependencies = [ "once_cell", "quickcheck", "quickcheck_macros", + "radicle-crypto", "radicle-git-ext", "radicle-ssh", "serde", @@ -1104,6 +1105,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "radicle-crypto" +version = "0.1.0" +dependencies = [ + "base64", + "ed25519-compact", + "fastrand", + "git-ref-format", + "multibase", + "quickcheck", + "quickcheck_macros", + "radicle-ssh", + "serde", + "sha2 0.10.6", + "sqlite", + "thiserror", + "zeroize", +] + [[package]] name = "radicle-git-ext" version = "0.1.0" @@ -1171,6 +1191,7 @@ name = "radicle-remote-helper" version = "0.2.0" dependencies = [ "radicle", + "radicle-crypto", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 02119d5a..d299b057 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "radicle", + "radicle-crypto", "radicle-node", "radicle-tools", "radicle-ssh", diff --git a/radicle-crypto/Cargo.toml b/radicle-crypto/Cargo.toml new file mode 100644 index 00000000..7aef8a24 --- /dev/null +++ b/radicle-crypto/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "radicle-crypto" +license = "MIT OR Apache-2.0" +version = "0.1.0" +authors = [ + "Alexis Sellier ", + "Fintan Halpenny ", +] +edition = "2021" + +[features] +test = ["fastrand", "quickcheck"] +ssh = ["base64", "radicle-ssh", "sha2"] + +[dependencies] +ed25519-compact = { version = "1.0.12", features = ["pem"] } +multibase = { version = "0.9.1" } +serde = { version = "1", features = ["derive"] } +sqlite = { version = "0.27.0", optional = true } +thiserror = { version = "1" } + +[dependencies.fastrand] +version = "1.8.0" +default-features = false +optional = true + +[dependencies.git-ref-format] +version = "0.1" +optional = true + +[dependencies.quickcheck] +version = "1" +default-features = false +optional = true + +[dependencies.radicle-ssh] +path = "../radicle-ssh" +version = "0" +default-features = false +optional = true + +[dependencies.sha2] +version = "0.10.2" +optional = true + +[dependencies.base64] +version = "0.13" +optional = true + + +[dev-dependencies] +fastrand = { version = "1.8.0" } +quickcheck_macros = { version = "1", default-features = false } +quickcheck = { version = "1", default-features = false } +zeroize = { version = "1.5.7" } diff --git a/radicle/src/crypto.rs b/radicle-crypto/src/lib.rs similarity index 86% rename from radicle/src/crypto.rs rename to radicle-crypto/src/lib.rs index 6ecfda0f..85ae0fcc 100644 --- a/radicle/src/crypto.rs +++ b/radicle-crypto/src/lib.rs @@ -7,6 +7,12 @@ use thiserror::Error; pub use ed25519::{Error, KeyPair, Seed}; +#[cfg(any(test, feature = "test"))] +pub mod test; + +#[cfg(feature = "ssh")] +pub mod ssh; + /// Verified (used as type witness). #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Verified; @@ -250,9 +256,37 @@ impl Deref for PublicKey { } } +#[cfg(feature = "git-ref-format")] +impl<'a> From<&PublicKey> for git_ref_format::Component<'a> { + fn from(id: &PublicKey) -> Self { + use git_ref_format::{Component, RefString}; + let refstr = + RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings"); + Component::from_refstring(refstr).expect("encoded public keys are valid refname components") + } +} + +#[cfg(feature = "sqlite")] +impl sqlite::ValueInto for PublicKey { + fn into(value: &sqlite::Value) -> Option { + use sqlite::Value; + match value { + Value::String(id) => PublicKey::from_str(id).ok(), + _ => None, + } + } +} + +#[cfg(feature = "sqlite")] +impl sqlite::Bindable for &PublicKey { + fn bind(self, stmt: &mut sqlite::Statement<'_>, i: usize) -> sqlite::Result<()> { + self.to_human().as_str().bind(stmt, i) + } +} + #[cfg(test)] -mod test { - use crate::crypto::PublicKey; +mod tests { + use crate::PublicKey; use quickcheck_macros::quickcheck; use std::str::FromStr; diff --git a/radicle/src/ssh.rs b/radicle-crypto/src/ssh.rs similarity index 98% rename from radicle/src/ssh.rs rename to radicle-crypto/src/ssh.rs index 483db5fe..d27d57ec 100644 --- a/radicle/src/ssh.rs +++ b/radicle-crypto/src/ssh.rs @@ -9,13 +9,13 @@ use radicle_ssh::encoding::Encodable; use radicle_ssh::encoding::Encoding; use radicle_ssh::encoding::Reader; -use crate::crypto; -use crate::crypto::PublicKey; +use crate as crypto; +use crate::PublicKey; pub mod fmt { use radicle_ssh::encoding::Encoding as _; - use crate::crypto::PublicKey; + use crate::PublicKey; /// Get the SSH long key from a public key. /// This is the output of `ssh-add -L`. @@ -49,7 +49,7 @@ pub mod fmt { use std::str::FromStr; use super::*; - use crate::crypto::PublicKey; + use crate::PublicKey; #[test] fn test_key() { @@ -319,9 +319,9 @@ mod test { use zeroize::Zeroizing; use super::{fmt, ExtendedSignature, SecretKey}; - use crate::crypto::PublicKey; - use crate::crypto::{self}; + use crate as crypto; use crate::test::arbitrary::ByteArray; + use crate::PublicKey; use radicle_ssh::agent::client::{AgentClient, ClientStream, Error}; use radicle_ssh::encoding::*; diff --git a/radicle/src/ssh/agent.rs b/radicle-crypto/src/ssh/agent.rs similarity index 96% rename from radicle/src/ssh/agent.rs rename to radicle-crypto/src/ssh/agent.rs index c75ca0c0..3feafdc1 100644 --- a/radicle/src/ssh/agent.rs +++ b/radicle-crypto/src/ssh/agent.rs @@ -1,7 +1,7 @@ use radicle_ssh::agent::client::AgentClient; use radicle_ssh::{self as ssh, agent::client::ClientStream}; -use crate::crypto; +use crate as crypto; use crate::ssh::SecretKey; #[cfg(not(unix))] diff --git a/radicle-crypto/src/test.rs b/radicle-crypto/src/test.rs new file mode 100644 index 00000000..cadc1f12 --- /dev/null +++ b/radicle-crypto/src/test.rs @@ -0,0 +1,2 @@ +pub mod arbitrary; +pub mod signer; diff --git a/radicle-crypto/src/test/arbitrary.rs b/radicle-crypto/src/test/arbitrary.rs new file mode 100644 index 00000000..25443de7 --- /dev/null +++ b/radicle-crypto/src/test/arbitrary.rs @@ -0,0 +1,46 @@ +use quickcheck::Arbitrary; + +use crate::{test::signer::MockSigner, KeyPair, PublicKey, Seed}; + +impl Arbitrary for MockSigner { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let bytes: ByteArray<32> = Arbitrary::arbitrary(g); + let seed = Seed::new(bytes.into_inner()); + let sk = KeyPair::from_seed(seed).sk; + + MockSigner::from(sk) + } +} + +impl Arbitrary for PublicKey { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let bytes: ByteArray<32> = Arbitrary::arbitrary(g); + let seed = Seed::new(bytes.into_inner()); + let keypair = KeyPair::from_seed(seed); + + PublicKey(keypair.pk) + } +} + +#[derive(Clone, Debug)] +pub struct ByteArray([u8; N]); + +impl ByteArray { + pub fn into_inner(self) -> [u8; N] { + self.0 + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl Arbitrary for ByteArray { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut bytes: [u8; N] = [0; N]; + for byte in &mut bytes { + *byte = u8::arbitrary(g); + } + Self(bytes) + } +} diff --git a/radicle/src/test/signer.rs b/radicle-crypto/src/test/signer.rs similarity index 94% rename from radicle/src/test/signer.rs rename to radicle-crypto/src/test/signer.rs index 486b7901..db518fb4 100644 --- a/radicle/src/test/signer.rs +++ b/radicle-crypto/src/test/signer.rs @@ -1,4 +1,4 @@ -use crate::crypto::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer}; +use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer}; #[derive(Debug, Clone)] pub struct MockSigner { diff --git a/radicle-node/src/service/message.rs b/radicle-node/src/service/message.rs index 7d029601..f7ca7056 100644 --- a/radicle-node/src/service/message.rs +++ b/radicle-node/src/service/message.rs @@ -432,7 +432,7 @@ mod tests { use super::*; use quickcheck_macros::quickcheck; - use crate::test::signer::MockSigner; + use crate::crypto::test::signer::MockSigner; #[quickcheck] fn prop_refs_announcement_signing(id: Id, refs: Refs) { diff --git a/radicle-node/src/test/gossip.rs b/radicle-node/src/test/gossip.rs index 432da8d2..b7908792 100644 --- a/radicle-node/src/test/gossip.rs +++ b/radicle-node/src/test/gossip.rs @@ -1,4 +1,4 @@ -use radicle::test::signer::MockSigner; +use radicle::crypto::test::signer::MockSigner; use crate::test::arbitrary; use crate::{ diff --git a/radicle-node/src/test/peer.rs b/radicle-node/src/test/peer.rs index b575892c..c5689194 100644 --- a/radicle-node/src/test/peer.rs +++ b/radicle-node/src/test/peer.rs @@ -8,6 +8,7 @@ use log::*; use crate::address; use crate::clock::{RefClock, Timestamp}; +use crate::crypto::test::signer::MockSigner; use crate::crypto::Signer; use crate::identity::Id; use crate::node; @@ -20,7 +21,6 @@ use crate::service::*; use crate::storage::git::transport::remote; use crate::storage::{RemoteId, WriteStorage}; use crate::test::arbitrary; -use crate::test::signer::MockSigner; use crate::test::simulator; use crate::{Link, LocalDuration, LocalTime}; diff --git a/radicle-node/src/test/tests.rs b/radicle-node/src/test/tests.rs index a82fe14b..b6fa8e8e 100644 --- a/radicle-node/src/test/tests.rs +++ b/radicle-node/src/test/tests.rs @@ -5,6 +5,7 @@ use crossbeam_channel as chan; use nakamoto_net as nakamoto; use crate::collections::{HashMap, HashSet}; +use crate::crypto::test::signer::MockSigner; use crate::prelude::{LocalDuration, Timestamp}; use crate::service::config::*; use crate::service::filter::Filter; @@ -22,7 +23,6 @@ use crate::test::fixtures; #[allow(unused)] use crate::test::logger; use crate::test::peer::Peer; -use crate::test::signer::MockSigner; use crate::test::simulator; use crate::test::simulator::{Peer as _, Simulation}; use crate::test::storage::MockStorage; diff --git a/radicle-remote-helper/Cargo.toml b/radicle-remote-helper/Cargo.toml index b8bd653a..cf4ff16b 100644 --- a/radicle-remote-helper/Cargo.toml +++ b/radicle-remote-helper/Cargo.toml @@ -12,6 +12,10 @@ thiserror = "1" path = "../radicle" version = "0" +[dependencies.radicle-crypto] +path = "../radicle-crypto" +version = "0" + [[bin]] name = "git-remote-rad" path = "src/git-remote-rad.rs" diff --git a/radicle-remote-helper/src/lib.rs b/radicle-remote-helper/src/lib.rs index 7f460969..c189b8aa 100644 --- a/radicle-remote-helper/src/lib.rs +++ b/radicle-remote-helper/src/lib.rs @@ -4,9 +4,9 @@ use std::{env, io, process}; use thiserror::Error; +use radicle::crypto::ssh; use radicle::crypto::{PublicKey, Signer}; use radicle::node::Handle; -use radicle::ssh; use radicle::storage::git::transport::local::{Url, UrlError}; use radicle::storage::{ReadRepository, WriteRepository, WriteStorage}; diff --git a/radicle-tools/src/rad-agent.rs b/radicle-tools/src/rad-agent.rs index 8f0ec199..f93cb44d 100644 --- a/radicle-tools/src/rad-agent.rs +++ b/radicle-tools/src/rad-agent.rs @@ -1,4 +1,4 @@ -use radicle::{crypto, ssh}; +use radicle::{crypto, crypto::ssh}; use std::io::prelude::*; use std::{env, io}; diff --git a/radicle-tools/src/rad-auth.rs b/radicle-tools/src/rad-auth.rs index e0391702..9f7673cf 100644 --- a/radicle-tools/src/rad-auth.rs +++ b/radicle-tools/src/rad-auth.rs @@ -1,6 +1,6 @@ fn main() -> anyhow::Result<()> { let keypair = radicle::crypto::KeyPair::generate(); - radicle::ssh::agent::register(&keypair.sk)?; + radicle::crypto::ssh::agent::register(&keypair.sk)?; let profile = radicle::Profile::init(keypair)?; diff --git a/radicle-tools/src/rad-self.rs b/radicle-tools/src/rad-self.rs index d9cf7fa0..fe65e015 100644 --- a/radicle-tools/src/rad-self.rs +++ b/radicle-tools/src/rad-self.rs @@ -2,10 +2,10 @@ fn main() -> anyhow::Result<()> { let profile = radicle::Profile::load()?; println!("id: {}", profile.id()); - println!("key: {}", radicle::ssh::fmt::key(profile.id())); + println!("key: {}", radicle::crypto::ssh::fmt::key(profile.id())); println!( "fingerprint: {}", - radicle::ssh::fmt::fingerprint(profile.id()) + radicle::crypto::ssh::fmt::fingerprint(profile.id()) ); println!("home: {}", profile.home.display()); diff --git a/radicle/Cargo.toml b/radicle/Cargo.toml index d1c84bfd..70aeba41 100644 --- a/radicle/Cargo.toml +++ b/radicle/Cargo.toml @@ -38,6 +38,11 @@ version = "0.15.0" default-features = false features = ["vendored-libgit2"] +[dependencies.radicle-crypto] +path = "../radicle-crypto" +version = "0" +features = ["git-ref-format", "ssh", "sqlite"] + [dependencies.radicle-ssh] path = "../radicle-ssh" version = "0" @@ -51,3 +56,8 @@ optional = true [dev-dependencies] quickcheck_macros = { version = "1", default-features = false } quickcheck = { version = "1", default-features = false } + +[dev-dependencies.radicle-crypto] +path = "../radicle-crypto" +version = "0" +features = ["test"] diff --git a/radicle/src/git.rs b/radicle/src/git.rs index 1521aaaa..7dec3bf7 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -57,14 +57,6 @@ pub enum ListRefsError { InvalidRef(#[from] RefError), } -impl<'a> From<&RemoteId> for Component<'a> { - fn from(id: &RemoteId) -> Self { - let refstr = - RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings"); - Component::from_refstring(refstr).expect("encoded public keys are valid refname components") - } -} - pub mod refs { use super::*; diff --git a/radicle/src/identity/project.rs b/radicle/src/identity/project.rs index c1935a92..3b880231 100644 --- a/radicle/src/identity/project.rs +++ b/radicle/src/identity/project.rs @@ -488,13 +488,14 @@ impl Identity { #[cfg(test)] mod test { - use crate::crypto::Signer; + use radicle_crypto::test::signer::MockSigner; + use radicle_crypto::Signer; + use crate::rad; use crate::storage::git::Storage; use crate::storage::{ReadStorage, WriteStorage}; use crate::test::arbitrary; use crate::test::fixtures; - use crate::test::signer::MockSigner; use super::*; use quickcheck_macros::quickcheck; diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index 1091006e..de16aa0a 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -1,7 +1,9 @@ #![allow(clippy::match_like_matches_macro)] #![cfg_attr(not(test), warn(clippy::unwrap_used))] + +pub extern crate radicle_crypto as crypto; + pub mod collections; -pub mod crypto; pub mod git; pub mod hash; pub mod identity; @@ -12,7 +14,6 @@ pub mod rad; pub mod serde_ext; #[cfg(feature = "sql")] pub mod sql; -pub mod ssh; pub mod storage; #[cfg(any(test, feature = "test"))] pub mod test; diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 02ac3fac..7ac4993c 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -321,13 +321,15 @@ pub fn remote(repo: &git2::Repository) -> Result<(git2::Remote<'_>, Id), RemoteE mod tests { use std::collections::HashMap; + use radicle_crypto::test::signer::MockSigner; + use super::*; use crate::git::fmt::refname; use crate::identity::{Delegate, Did}; use crate::storage::git::transport; use crate::storage::git::Storage; use crate::storage::{ReadStorage, WriteStorage}; - use crate::test::{fixtures, signer::MockSigner}; + use crate::test::fixtures; #[test] fn test_init() { diff --git a/radicle/src/sql.rs b/radicle/src/sql.rs index cb79206c..f0162793 100644 --- a/radicle/src/sql.rs +++ b/radicle/src/sql.rs @@ -4,7 +4,6 @@ use std::str::FromStr; use sqlite as sql; use sqlite::Value; -use crate::crypto::PublicKey; use crate::identity::Id; use crate::node; @@ -23,21 +22,6 @@ impl sqlite::Bindable for &Id { } } -impl sql::ValueInto for PublicKey { - fn into(value: &Value) -> Option { - match value { - Value::String(id) => PublicKey::from_str(id).ok(), - _ => None, - } - } -} - -impl sqlite::Bindable for &PublicKey { - fn bind(self, stmt: &mut sql::Statement<'_>, i: usize) -> sql::Result<()> { - self.to_human().as_str().bind(stmt, i) - } -} - impl sql::Bindable for node::Features { fn bind(self, stmt: &mut sql::Statement<'_>, i: usize) -> sql::Result<()> { (*self.deref() as i64).bind(stmt, i) diff --git a/radicle/src/storage.rs b/radicle/src/storage.rs index 1854040e..6dad0c6d 100644 --- a/radicle/src/storage.rs +++ b/radicle/src/storage.rs @@ -8,12 +8,11 @@ use std::{fmt, io}; use thiserror::Error; +use crypto::{PublicKey, Signer, Unverified, Verified}; pub use git::{ProjectError, VerifyError}; pub use radicle_git_ext::Oid; use crate::collections::HashMap; -use crate::crypto; -use crate::crypto::{PublicKey, Signer, Unverified, Verified}; use crate::git::ext as git_ext; use crate::git::{Qualified, RefError, RefString}; use crate::identity; diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 53e5a701..be5b2e86 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -4,10 +4,10 @@ use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::{fs, io}; +use crypto::{Signer, Unverified, Verified}; use git_ref_format::refspec; use once_cell::sync::Lazy; -use crate::crypto::{Signer, Unverified, Verified}; use crate::git; use crate::identity; use crate::identity::project::{Identity, IdentityError}; @@ -664,8 +664,8 @@ pub mod trailers { use std::str::FromStr; use super::*; - use crate::crypto::{PublicKey, PublicKeyError}; - use crate::crypto::{Signature, SignatureError}; + use crypto::{PublicKey, PublicKeyError}; + use crypto::{Signature, SignatureError}; pub const SIGNATURE_TRAILER: &str = "Rad-Signature"; @@ -716,6 +716,8 @@ mod tests { use std::io::{Read, Write}; use std::{io, net, process, thread}; + use crypto::test::signer::MockSigner; + use super::*; use crate::assert_matches; use crate::git; @@ -724,7 +726,6 @@ mod tests { use crate::storage::{ReadRepository, ReadStorage, RefUpdate, WriteRepository}; use crate::test::arbitrary; use crate::test::fixtures; - use crate::test::signer::MockSigner; #[test] fn test_remote_refs() { diff --git a/radicle/src/storage/refs.rs b/radicle/src/storage/refs.rs index 4a744293..563a2073 100644 --- a/radicle/src/storage/refs.rs +++ b/radicle/src/storage/refs.rs @@ -7,12 +7,11 @@ use std::ops::{Deref, DerefMut}; use std::path::Path; use std::str::FromStr; +use crypto::{PublicKey, Signature, Signer, Unverified, Verified}; use once_cell::sync::Lazy; use radicle_git_ext as git_ext; use thiserror::Error; -use crate::crypto; -use crate::crypto::{PublicKey, Signature, Signer, Unverified, Verified}; use crate::git; use crate::git::Oid; use crate::storage; diff --git a/radicle/src/test.rs b/radicle/src/test.rs index 0ca2dccd..33343ecf 100644 --- a/radicle/src/test.rs +++ b/radicle/src/test.rs @@ -2,5 +2,4 @@ pub mod arbitrary; pub mod assert; pub mod fixtures; -pub mod signer; pub mod storage; diff --git a/radicle/src/test/arbitrary.rs b/radicle/src/test/arbitrary.rs index 121a8d79..f536e24e 100644 --- a/radicle/src/test/arbitrary.rs +++ b/radicle/src/test/arbitrary.rs @@ -3,18 +3,17 @@ use std::hash::Hash; use std::iter; use std::ops::RangeBounds; +use crypto::test::signer::MockSigner; +use crypto::{PublicKey, Signer, Unverified, Verified}; use nonempty::NonEmpty; use quickcheck::Arbitrary; use crate::collections::HashMap; -use crate::crypto; -use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified}; use crate::git; use crate::hash; use crate::identity::{project::Delegate, project::Doc, Did, Id}; use crate::storage; use crate::storage::refs::{Refs, SignedRefs}; -use crate::test::signer::MockSigner; use crate::test::storage::MockStorage; pub fn set(range: impl RangeBounds) -> HashSet { @@ -174,16 +173,6 @@ impl Arbitrary for Refs { } } -impl Arbitrary for MockSigner { - fn arbitrary(g: &mut quickcheck::Gen) -> Self { - let bytes: ByteArray<32> = Arbitrary::arbitrary(g); - let seed = Seed::new(bytes.into_inner()); - let sk = KeyPair::from_seed(seed).sk; - - MockSigner::from(sk) - } -} - impl Arbitrary for MockStorage { fn arbitrary(g: &mut quickcheck::Gen) -> Self { let inventory = Arbitrary::arbitrary(g); @@ -216,13 +205,3 @@ impl Arbitrary for hash::Digest { hash::Digest::new(&bytes) } } - -impl Arbitrary for PublicKey { - fn arbitrary(g: &mut quickcheck::Gen) -> Self { - let bytes: ByteArray<32> = Arbitrary::arbitrary(g); - let seed = Seed::new(bytes.into_inner()); - let keypair = KeyPair::from_seed(seed); - - PublicKey(keypair.pk) - } -}