Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle-crypto: split out from radicle
Fintan Halpenny committed 3 years ago
commit c0b0d29e19c0993098103a940f99be0d8aa4d76a
parent 2a1154ef87fb35ef62550345762d7ee0b6dfebdb
33 files changed +1022 -892
modified Cargo.lock
@@ -1092,6 +1092,7 @@ dependencies = [
 "once_cell",
 "quickcheck",
 "quickcheck_macros",
+
 "radicle-crypto",
 "radicle-git-ext",
 "radicle-ssh",
 "serde",
@@ -1105,6 +1106,25 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1171,6 +1191,7 @@ name = "radicle-remote-helper"
version = "0.2.0"
dependencies = [
 "radicle",
+
 "radicle-crypto",
 "thiserror",
]

modified Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = [
  "radicle",
+
  "radicle-crypto",
  "radicle-node",
  "radicle-tools",
  "radicle-ssh",
added 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 <alexis@radicle.xyz>",
+
  "Fintan Halpenny <fintan.halpenny@gmail.com>",
+
]
+
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" }
added radicle-crypto/src/lib.rs
@@ -0,0 +1,308 @@
+
use std::sync::Arc;
+
use std::{fmt, ops::Deref, str::FromStr};
+

+
use ed25519_compact as ed25519;
+
use serde::{Deserialize, Serialize};
+
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;
+
/// Unverified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub struct Unverified;
+

+
pub trait Signer: Send + Sync {
+
    /// Return this signer's public/verification key.
+
    fn public_key(&self) -> &PublicKey;
+
    /// Sign a message and return the signature.
+
    fn sign(&self, msg: &[u8]) -> Signature;
+
}
+

+
impl<T> Signer for Arc<T>
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+
}
+

+
impl<T> Signer for &T
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+
}
+

+
/// Cryptographic signature.
+
#[derive(PartialEq, Eq, Copy, Clone)]
+
pub struct Signature(pub ed25519::Signature);
+

+
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()))
+
    }
+
}
+

+
impl fmt::Debug for Signature {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Signature({})", self)
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum SignatureError {
+
    #[error("invalid multibase string: {0}")]
+
    Multibase(#[from] multibase::Error),
+
    #[error("invalid signature: {0}")]
+
    Invalid(#[from] ed25519::Error),
+
}
+

+
impl From<ed25519::Signature> for Signature {
+
    fn from(other: ed25519::Signature) -> Self {
+
        Self(other)
+
    }
+
}
+

+
impl FromStr for Signature {
+
    type Err = SignatureError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
+

+
        Ok(Self(sig))
+
    }
+
}
+

+
impl Deref for Signature {
+
    type Target = ed25519::Signature;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl From<[u8; 64]> for Signature {
+
    fn from(bytes: [u8; 64]) -> Self {
+
        Self(ed25519::Signature::new(bytes))
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Signature {
+
    type Error = ed25519::Error;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+
        ed25519::Signature::from_slice(bytes).map(Self)
+
    }
+
}
+

+
/// The public/verification key.
+
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
+
#[serde(into = "String", try_from = "String")]
+
pub struct PublicKey(pub ed25519::PublicKey);
+

+
impl PublicKey {
+
    pub fn from_pem(pem: &str) -> Result<Self, ed25519::Error> {
+
        ed25519::PublicKey::from_pem(pem).map(Self)
+
    }
+
}
+

+
/// The private/signing key.
+
pub type SecretKey = ed25519::SecretKey;
+

+
#[derive(Error, Debug)]
+
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),
+
}
+

+
impl std::hash::Hash for PublicKey {
+
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
        self.0.deref().hash(state)
+
    }
+
}
+

+
impl PartialOrd for PublicKey {
+
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+
        self.0.as_ref().partial_cmp(other.as_ref())
+
    }
+
}
+

+
impl Ord for PublicKey {
+
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+
        self.0.as_ref().cmp(other.as_ref())
+
    }
+
}
+

+
impl fmt::Display for PublicKey {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", self.to_human())
+
    }
+
}
+

+
impl From<PublicKey> for String {
+
    fn from(other: PublicKey) -> Self {
+
        other.to_human()
+
    }
+
}
+

+
impl fmt::Debug for PublicKey {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "PublicKey({})", self)
+
    }
+
}
+

+
impl PartialEq for PublicKey {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.0 == other.0
+
    }
+
}
+

+
impl From<ed25519::PublicKey> for PublicKey {
+
    fn from(other: ed25519::PublicKey) -> Self {
+
        Self(other)
+
    }
+
}
+

+
impl From<[u8; 32]> for PublicKey {
+
    fn from(other: [u8; 32]) -> Self {
+
        Self(ed25519::PublicKey::new(other))
+
    }
+
}
+

+
impl TryFrom<&[u8]> for PublicKey {
+
    type Error = ed25519::Error;
+

+
    fn try_from(other: &[u8]) -> Result<Self, Self::Error> {
+
        ed25519::PublicKey::from_slice(other).map(Self)
+
    }
+
}
+

+
impl PublicKey {
+
    /// Multicodec key type for Ed25519 keys.
+
    pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x1];
+

+
    /// Encode public key in human-readable format.
+
    ///
+
    /// We use the format specified by the DID `key` method, which is described as:
+
    ///
+
    /// `did:key: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.0.deref());
+

+
        multibase::encode(multibase::Base::Base58Btc, &buf)
+
    }
+
}
+

+
impl FromStr for PublicKey {
+
    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(Self(key))
+
        } else {
+
            Err(PublicKeyError::Multicodec(Self::MULTICODEC_TYPE))
+
        }
+
    }
+
}
+

+
impl TryFrom<String> for PublicKey {
+
    type Error = PublicKeyError;
+

+
    fn try_from(value: String) -> Result<Self, Self::Error> {
+
        Self::from_str(&value)
+
    }
+
}
+

+
impl Deref for PublicKey {
+
    type Target = ed25519::PublicKey;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
#[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<Self> {
+
        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 tests {
+
    use crate::PublicKey;
+
    use quickcheck_macros::quickcheck;
+
    use std::str::FromStr;
+

+
    #[quickcheck]
+
    fn prop_encode_decode(input: PublicKey) {
+
        let encoded = input.to_string();
+
        let decoded = PublicKey::from_str(&encoded).unwrap();
+

+
        assert_eq!(input, decoded);
+
    }
+

+
    #[test]
+
    fn test_encode_decode() {
+
        let input = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
+
        let key = PublicKey::from_str(input).unwrap();
+

+
        assert_eq!(key.to_string(), input);
+
    }
+
}
added radicle-crypto/src/ssh.rs
@@ -0,0 +1,462 @@
+
pub mod agent;
+

+
use std::io;
+

+
use thiserror::Error;
+

+
use radicle_ssh::encoding;
+
use radicle_ssh::encoding::Encodable;
+
use radicle_ssh::encoding::Encoding;
+
use radicle_ssh::encoding::Reader;
+

+
use crate as crypto;
+
use crate::PublicKey;
+

+
pub mod fmt {
+
    use radicle_ssh::encoding::Encoding as _;
+

+
    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 {
+
        let mut buf = Vec::new();
+

+
        buf.extend_ssh_string(b"ssh-ed25519");
+
        buf.extend_ssh_string(key.as_ref());
+

+
        base64::encode_config(buf, base64::STANDARD_NO_PAD)
+
    }
+

+
    /// Get the SSH key fingerprint from a public key.
+
    /// This is the output of `ssh-add -l`.
+
    pub fn fingerprint(key: &PublicKey) -> String {
+
        use sha2::Digest;
+

+
        let mut buf = Vec::new();
+

+
        buf.extend_ssh_string(b"ssh-ed25519");
+
        buf.extend_ssh_string(key.as_ref());
+

+
        let sha = sha2::Sha256::digest(&buf).to_vec();
+
        let encoded = base64::encode_config(sha, base64::STANDARD_NO_PAD);
+

+
        format!("SHA256:{}", encoded)
+
    }
+

+
    #[cfg(test)]
+
    mod test {
+
        use std::str::FromStr;
+

+
        use super::*;
+
        use crate::PublicKey;
+

+
        #[test]
+
        fn test_key() {
+
            let pk =
+
                PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+

+
            assert_eq!(
+
                key(&pk),
+
                "AAAAC3NzaC1lZDI1NTE5AAAAINDoXIrhcnRjnLGUXUFdxhkuy08lkTOwrj2IoGsEX6+Q"
+
            );
+
        }
+

+
        #[test]
+
        fn test_fingerprint() {
+
            let pk =
+
                PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+
            assert_eq!(
+
                fingerprint(&pk),
+
                "SHA256:gE/Ty4fuXzww49lcnNe9/GI0L7xSEQdFp/v9tOjFwB4"
+
            );
+
        }
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum SignatureError {
+
    #[error(transparent)]
+
    Invalid(#[from] crypto::Error),
+
    #[error(transparent)]
+
    Encoding(#[from] encoding::Error),
+
    #[error("unknown algorithm '{0}'")]
+
    UnknownAlgorithm(String),
+
}
+

+
impl Encodable for crypto::Signature {
+
    type Error = SignatureError;
+

+
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
+
        let buf = r.read_string()?;
+
        let mut inner_strs = buf.reader(0);
+

+
        let sig_type = inner_strs.read_string()?;
+
        if sig_type != b"ssh-ed25519" {
+
            return Err(SignatureError::UnknownAlgorithm(
+
                String::from_utf8_lossy(sig_type).to_string(),
+
            ));
+
        }
+
        let sig = crypto::Signature::try_from(inner_strs.read_string()?)?;
+

+
        Ok(sig)
+
    }
+

+
    fn write<E: Encoding>(&self, buf: &mut E) {
+
        let mut inner_strs = Vec::new();
+
        inner_strs.extend_ssh_string(b"ssh-ed25519");
+
        inner_strs.extend_ssh_string(self.as_ref());
+
        buf.extend_ssh_string(&inner_strs);
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum PublicKeyError {
+
    #[error(transparent)]
+
    Invalid(#[from] crypto::Error),
+
    #[error(transparent)]
+
    Encoding(#[from] encoding::Error),
+
    #[error("unknown algorithm '{0}'")]
+
    UnknownAlgorithm(String),
+
}
+

+
impl Encodable for PublicKey {
+
    type Error = PublicKeyError;
+

+
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
+
        match r.read_string()? {
+
            b"ssh-ed25519" => {
+
                let s = r.read_string()?;
+
                let p = PublicKey::try_from(s)?;
+

+
                Ok(p)
+
            }
+
            v => Err(PublicKeyError::UnknownAlgorithm(
+
                String::from_utf8_lossy(v).to_string(),
+
            )),
+
        }
+
    }
+

+
    fn write<E: Encoding>(&self, w: &mut E) {
+
        let mut str_w: Vec<u8> = Vec::<u8>::new();
+
        str_w.extend_ssh_string(b"ssh-ed25519");
+
        str_w.extend_ssh_string(&self[..]);
+
        w.extend_ssh_string(&str_w)
+
    }
+
}
+

+
// FIXME: Should zeroize, or we should be creating our own type
+
// in `crypto`.
+
struct SecretKey(crypto::SecretKey);
+

+
impl From<crypto::SecretKey> for SecretKey {
+
    fn from(other: crypto::SecretKey) -> Self {
+
        Self(other)
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum SecretKeyError {
+
    #[error(transparent)]
+
    Encoding(#[from] encoding::Error),
+
    #[error(transparent)]
+
    Crypto(#[from] crypto::Error),
+
    #[error(transparent)]
+
    Io(#[from] io::Error),
+
    #[error("unknown algorithm '{0}'")]
+
    UnknownAlgorithm(String),
+
    #[error("public key does not match secret key")]
+
    Mismatch,
+
}
+

+
impl Encodable for SecretKey {
+
    type Error = SecretKeyError;
+

+
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
+
        match r.read_string()? {
+
            b"ssh-ed25519" => {
+
                let public = r.read_string()?;
+
                let pair = r.read_string()?;
+
                let _comment = r.read_string()?;
+
                let key = crypto::SecretKey::from_slice(pair)?;
+

+
                if public != key.public_key().as_ref() {
+
                    return Err(SecretKeyError::Mismatch);
+
                }
+
                Ok(SecretKey(key))
+
            }
+
            s => Err(SecretKeyError::UnknownAlgorithm(
+
                String::from_utf8_lossy(s).to_string(),
+
            )),
+
        }
+
    }
+

+
    fn write<E: Encoding>(&self, buf: &mut E) {
+
        let public = self.0.public_key();
+

+
        buf.extend_ssh_string(b"ssh-ed25519");
+
        buf.extend_ssh_string(public.as_ref());
+
        buf.extend_ssh_string(&*self.0);
+
        buf.extend_ssh_string(b"radicle");
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum ExtendedSignatureError {
+
    #[error(transparent)]
+
    Base64Encoding(#[from] base64::DecodeError),
+
    #[error("wrong preamble")]
+
    MagicPreamble([u8; 6]),
+
    #[error("missing armored footer")]
+
    MissingFooter,
+
    #[error("missing armored header")]
+
    MissingHeader,
+
    #[error(transparent)]
+
    Encoding(#[from] encoding::Error),
+
    #[error(transparent)]
+
    PublicKey(#[from] PublicKeyError),
+
    #[error(transparent)]
+
    SignatureError(#[from] SignatureError),
+
    #[error("unsupported version '{0}'")]
+
    UnsupportedVersion(u32),
+
}
+

+
/// An SSH signature's decoded format.
+
///
+
/// See <https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig>
+
#[derive(Clone, Debug)]
+
pub struct ExtendedSignature {
+
    version: u32,
+
    public_key: crypto::PublicKey,
+
    /// Unambigious interpretation domain to prevent cross-protocol attacks.
+
    namespace: Vec<u8>,
+
    reserved: Vec<u8>,
+
    /// Hash used for signature. For example 'sha256'.
+
    hash_algorithm: Vec<u8>,
+
    signature: crypto::Signature,
+
}
+

+
impl Encodable for ExtendedSignature {
+
    type Error = ExtendedSignatureError;
+

+
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
+
        let sig_version = r.read_u32()?;
+
        if sig_version > 1 {
+
            return Err(ExtendedSignatureError::UnsupportedVersion(sig_version));
+
        }
+
        let mut pk = r.read_string()?.reader(0);
+

+
        Ok(ExtendedSignature {
+
            version: sig_version,
+
            public_key: PublicKey::read(&mut pk)?,
+
            namespace: r.read_string()?.into(),
+
            reserved: r.read_string()?.into(),
+
            hash_algorithm: r.read_string()?.into(),
+
            signature: crypto::Signature::read(r)?,
+
        })
+
    }
+

+
    fn write<E: Encoding>(&self, buf: &mut E) {
+
        buf.extend_u32(self.version);
+
        let _ = &self.public_key.write(buf);
+
        buf.extend_ssh_string(&self.namespace);
+
        buf.extend_ssh_string(&self.reserved);
+
        buf.extend_ssh_string(&self.hash_algorithm);
+
        let _ = &self.signature.write(buf);
+
    }
+
}
+

+
impl ExtendedSignature {
+
    const ARMORED_HEADER: &[u8] = b"-----BEGIN SSH SIGNATURE-----";
+
    const ARMORED_FOOTER: &[u8] = b"-----END SSH SIGNATURE-----";
+
    const ARMORED_WIDTH: usize = 70;
+
    const MAGIC_PREAMBLE: &[u8] = b"SSHSIG";
+

+
    pub fn from_armored(s: &[u8]) -> Result<Self, ExtendedSignatureError> {
+
        let s = s
+
            .strip_prefix(Self::ARMORED_HEADER)
+
            .ok_or(ExtendedSignatureError::MissingHeader)?;
+
        let s = s
+
            .strip_suffix(Self::ARMORED_FOOTER)
+
            .ok_or(ExtendedSignatureError::MissingFooter)?;
+
        let s: Vec<u8> = s.iter().filter(|b| *b != &b'\n').copied().collect();
+

+
        let buf = base64::decode(s)?;
+
        let mut reader = buf.reader(0);
+

+
        let preamble: [u8; 6] = reader.read_bytes()?;
+
        if preamble != Self::MAGIC_PREAMBLE {
+
            return Err(ExtendedSignatureError::MagicPreamble(preamble));
+
        }
+

+
        let sig = ExtendedSignature::read(&mut reader)?;
+
        Ok(sig)
+
    }
+

+
    pub fn to_armored(&self) -> Vec<u8> {
+
        let mut buf = encoding::Buffer::from(Self::MAGIC_PREAMBLE.to_vec());
+
        self.write(&mut buf);
+

+
        let mut armored = Self::ARMORED_HEADER.to_vec();
+
        armored.push(b'\n');
+

+
        let body = base64::encode(buf);
+
        for line in body.as_bytes().chunks(Self::ARMORED_WIDTH) {
+
            armored.extend(line);
+
            armored.push(b'\n');
+
        }
+

+
        armored.extend(Self::ARMORED_FOOTER);
+
        armored
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use std::sync::{Arc, Mutex};
+

+
    use quickcheck_macros::quickcheck;
+
    use zeroize::Zeroizing;
+

+
    use super::{fmt, ExtendedSignature, SecretKey};
+
    use crate as crypto;
+
    use crate::test::arbitrary::ByteArray;
+
    use crate::PublicKey;
+
    use radicle_ssh::agent::client::{AgentClient, ClientStream, Error};
+
    use radicle_ssh::encoding::*;
+

+
    #[derive(Clone, Default)]
+
    struct DummyStream {
+
        incoming: Arc<Mutex<Zeroizing<Vec<u8>>>>,
+
    }
+

+
    impl ClientStream for DummyStream {
+
        fn connect_socket<P>(_path: P) -> Result<AgentClient<Self>, Error>
+
        where
+
            P: AsRef<std::path::Path> + Send,
+
        {
+
            panic!("This function should never be called!")
+
        }
+

+
        fn read_response(&mut self, buf: &mut Zeroizing<Vec<u8>>) -> Result<(), Error> {
+
            *self.incoming.lock().unwrap() = buf.clone();
+

+
            Ok(())
+
        }
+
    }
+

+
    #[quickcheck]
+
    fn prop_encode_decode_sk(input: ByteArray<64>) {
+
        let mut buf = Buffer::default();
+
        let sk = crypto::SecretKey::new(input.into_inner());
+
        SecretKey(sk).write(&mut buf);
+

+
        let mut cursor = buf.reader(0);
+
        let output = SecretKey::read(&mut cursor).unwrap();
+

+
        assert_eq!(sk, output.0);
+
    }
+

+
    #[test]
+
    fn test_agent_encoding_remove() {
+
        use std::str::FromStr;
+

+
        let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+
        let expected = [
+
            0, 0, 0, 56, // Message length
+
            18, // Message type (remove identity)
+
            0, 0, 0, 51, // Key blob length
+
            0, 0, 0, 11, // Key type length
+
            115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type
+
            0, 0, 0, 32, // Key length
+
            208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79,
+
            37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key
+
        ];
+

+
        let stream = DummyStream::default();
+
        let mut agent = AgentClient::connect(stream.clone());
+

+
        agent.remove_identity(&pk).unwrap();
+

+
        assert_eq!(
+
            stream.incoming.lock().unwrap().as_slice(),
+
            expected.as_slice()
+
        );
+
    }
+

+
    #[test]
+
    fn test_agent_encoding_sign() {
+
        use std::str::FromStr;
+

+
        let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+
        let expected = [
+
            0, 0, 0, 73, // Message length
+
            13, // Message type (sign request)
+
            0, 0, 0, 51, // Key blob length
+
            0, 0, 0, 11, // Key type length
+
            115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type
+
            0, 0, 0, 32, // Public key
+
            208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79,
+
            37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key
+
            0, 0, 0, 9, // Length of data to sign
+
            1, 2, 3, 4, 5, 6, 7, 8, 9, // Data to sign
+
            0, 0, 0, 0, // Signature flags
+
        ];
+

+
        let stream = DummyStream::default();
+
        let mut agent = AgentClient::connect(stream.clone());
+
        let data: Zeroizing<Vec<u8>> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9].into();
+

+
        agent.sign_request(&pk, data).ok();
+

+
        assert_eq!(
+
            stream.incoming.lock().unwrap().as_slice(),
+
            expected.as_slice()
+
        );
+
    }
+

+
    #[test]
+
    fn test_signature_encode_decode() {
+
        let armored: &[u8] = b"-----BEGIN SSH SIGNATURE-----
+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
+
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+
AAAAQI84aPZsXxlQigpy1/Y/iJSmHSS//CIgvqvUMQIb/TM2vhCKruduH0cK02k9G8wOI+
+
EUMf2bSDyxbJyZThOEiAs=
+
-----END SSH SIGNATURE-----";
+

+
        let public_key = "AAAAC3NzaC1lZDI1NTE5AAAAIL460KIEccS4881p7PPpiiQBsxF+H5tgC6De6crw9rbU";
+
        let signature = ExtendedSignature::from_armored(armored).unwrap();
+

+
        assert_eq!(signature.version, 1);
+
        assert_eq!(fmt::key(&signature.public_key), public_key);
+
        assert_eq!(
+
            String::from_utf8(armored.to_vec()),
+
            String::from_utf8(signature.to_armored()),
+
            "signature should remain unaltered after decoding"
+
        );
+
    }
+

+
    #[test]
+
    fn test_signature_verify() {
+
        let seed = crypto::Seed::new([1; 32]);
+
        let pair = crypto::KeyPair::from_seed(seed);
+
        let message = &[0xff];
+
        let sig = pair.sk.sign(message, None);
+
        let esig = ExtendedSignature {
+
            version: 1,
+
            public_key: pair.pk.into(),
+
            signature: sig.into(),
+
            hash_algorithm: vec![],
+
            namespace: vec![],
+
            reserved: vec![],
+
        };
+

+
        let armored = esig.to_armored();
+
        let unarmored = ExtendedSignature::from_armored(&armored).unwrap();
+

+
        unarmored
+
            .public_key
+
            .verify(message, &unarmored.signature)
+
            .unwrap();
+
    }
+
}
added radicle-crypto/src/ssh/agent.rs
@@ -0,0 +1,21 @@
+
use radicle_ssh::agent::client::AgentClient;
+
use radicle_ssh::{self as ssh, agent::client::ClientStream};
+

+
use crate as crypto;
+
use crate::ssh::SecretKey;
+

+
#[cfg(not(unix))]
+
use std::net::TcpStream as Stream;
+
#[cfg(unix)]
+
use std::os::unix::net::UnixStream as Stream;
+

+
pub fn connect() -> Result<AgentClient<Stream>, ssh::agent::client::Error> {
+
    Stream::connect_env()
+
}
+

+
pub fn register(key: &crypto::SecretKey) -> Result<(), ssh::agent::client::Error> {
+
    let mut agent = self::connect()?;
+
    agent.add_identity(&SecretKey::from(*key), &[])?;
+

+
    Ok(())
+
}
added radicle-crypto/src/test.rs
@@ -0,0 +1,2 @@
+
pub mod arbitrary;
+
pub mod signer;
added 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<const N: usize>([u8; N]);
+

+
impl<const N: usize> ByteArray<N> {
+
    pub fn into_inner(self) -> [u8; N] {
+
        self.0
+
    }
+

+
    pub fn as_slice(&self) -> &[u8] {
+
        self.0.as_slice()
+
    }
+
}
+

+
impl<const N: usize> Arbitrary for ByteArray<N> {
+
    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)
+
    }
+
}
added radicle-crypto/src/test/signer.rs
@@ -0,0 +1,65 @@
+
use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer};
+

+
#[derive(Debug, Clone)]
+
pub struct MockSigner {
+
    pk: PublicKey,
+
    sk: SecretKey,
+
}
+

+
impl MockSigner {
+
    pub fn new(rng: &mut fastrand::Rng) -> Self {
+
        let mut bytes: [u8; 32] = [0; 32];
+

+
        for byte in &mut bytes {
+
            *byte = rng.u8(..);
+
        }
+
        let seed = Seed::new(bytes);
+
        let keypair = KeyPair::from_seed(seed);
+

+
        Self::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,
+
        }
+
    }
+
}
+

+
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)
+
    }
+
}
+

+
impl Signer for MockSigner {
+
    fn public_key(&self) -> &PublicKey {
+
        &self.pk
+
    }
+

+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.sk.sign(msg, None).into()
+
    }
+
}
modified 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) {
modified 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::{
modified 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};

modified 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;
modified 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"
modified 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};

modified 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};

modified 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)?;

modified 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());

modified 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"]
deleted radicle/src/crypto.rs
@@ -1,274 +0,0 @@
-
use std::sync::Arc;
-
use std::{fmt, ops::Deref, str::FromStr};
-

-
use ed25519_compact as ed25519;
-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
pub use ed25519::{Error, KeyPair, Seed};
-

-
/// Verified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub struct Verified;
-
/// Unverified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub struct Unverified;
-

-
pub trait Signer: Send + Sync {
-
    /// Return this signer's public/verification key.
-
    fn public_key(&self) -> &PublicKey;
-
    /// Sign a message and return the signature.
-
    fn sign(&self, msg: &[u8]) -> Signature;
-
}
-

-
impl<T> Signer for Arc<T>
-
where
-
    T: Signer + ?Sized,
-
{
-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.deref().sign(msg)
-
    }
-

-
    fn public_key(&self) -> &PublicKey {
-
        self.deref().public_key()
-
    }
-
}
-

-
impl<T> Signer for &T
-
where
-
    T: Signer + ?Sized,
-
{
-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.deref().sign(msg)
-
    }
-

-
    fn public_key(&self) -> &PublicKey {
-
        self.deref().public_key()
-
    }
-
}
-

-
/// Cryptographic signature.
-
#[derive(PartialEq, Eq, Copy, Clone)]
-
pub struct Signature(pub ed25519::Signature);
-

-
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()))
-
    }
-
}
-

-
impl fmt::Debug for Signature {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Signature({})", self)
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum SignatureError {
-
    #[error("invalid multibase string: {0}")]
-
    Multibase(#[from] multibase::Error),
-
    #[error("invalid signature: {0}")]
-
    Invalid(#[from] ed25519::Error),
-
}
-

-
impl From<ed25519::Signature> for Signature {
-
    fn from(other: ed25519::Signature) -> Self {
-
        Self(other)
-
    }
-
}
-

-
impl FromStr for Signature {
-
    type Err = SignatureError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
-

-
        Ok(Self(sig))
-
    }
-
}
-

-
impl Deref for Signature {
-
    type Target = ed25519::Signature;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl From<[u8; 64]> for Signature {
-
    fn from(bytes: [u8; 64]) -> Self {
-
        Self(ed25519::Signature::new(bytes))
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Signature {
-
    type Error = ed25519::Error;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-
        ed25519::Signature::from_slice(bytes).map(Self)
-
    }
-
}
-

-
/// The public/verification key.
-
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
-
#[serde(into = "String", try_from = "String")]
-
pub struct PublicKey(pub ed25519::PublicKey);
-

-
impl PublicKey {
-
    pub fn from_pem(pem: &str) -> Result<Self, ed25519::Error> {
-
        ed25519::PublicKey::from_pem(pem).map(Self)
-
    }
-
}
-

-
/// The private/signing key.
-
pub type SecretKey = ed25519::SecretKey;
-

-
#[derive(Error, Debug)]
-
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),
-
}
-

-
impl std::hash::Hash for PublicKey {
-
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-
        self.0.deref().hash(state)
-
    }
-
}
-

-
impl PartialOrd for PublicKey {
-
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-
        self.0.as_ref().partial_cmp(other.as_ref())
-
    }
-
}
-

-
impl Ord for PublicKey {
-
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-
        self.0.as_ref().cmp(other.as_ref())
-
    }
-
}
-

-
impl fmt::Display for PublicKey {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.to_human())
-
    }
-
}
-

-
impl From<PublicKey> for String {
-
    fn from(other: PublicKey) -> Self {
-
        other.to_human()
-
    }
-
}
-

-
impl fmt::Debug for PublicKey {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "PublicKey({})", self)
-
    }
-
}
-

-
impl PartialEq for PublicKey {
-
    fn eq(&self, other: &Self) -> bool {
-
        self.0 == other.0
-
    }
-
}
-

-
impl From<ed25519::PublicKey> for PublicKey {
-
    fn from(other: ed25519::PublicKey) -> Self {
-
        Self(other)
-
    }
-
}
-

-
impl From<[u8; 32]> for PublicKey {
-
    fn from(other: [u8; 32]) -> Self {
-
        Self(ed25519::PublicKey::new(other))
-
    }
-
}
-

-
impl TryFrom<&[u8]> for PublicKey {
-
    type Error = ed25519::Error;
-

-
    fn try_from(other: &[u8]) -> Result<Self, Self::Error> {
-
        ed25519::PublicKey::from_slice(other).map(Self)
-
    }
-
}
-

-
impl PublicKey {
-
    /// Multicodec key type for Ed25519 keys.
-
    pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x1];
-

-
    /// Encode public key in human-readable format.
-
    ///
-
    /// We use the format specified by the DID `key` method, which is described as:
-
    ///
-
    /// `did:key: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.0.deref());
-

-
        multibase::encode(multibase::Base::Base58Btc, &buf)
-
    }
-
}
-

-
impl FromStr for PublicKey {
-
    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(Self(key))
-
        } else {
-
            Err(PublicKeyError::Multicodec(Self::MULTICODEC_TYPE))
-
        }
-
    }
-
}
-

-
impl TryFrom<String> for PublicKey {
-
    type Error = PublicKeyError;
-

-
    fn try_from(value: String) -> Result<Self, Self::Error> {
-
        Self::from_str(&value)
-
    }
-
}
-

-
impl Deref for PublicKey {
-
    type Target = ed25519::PublicKey;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use crate::crypto::PublicKey;
-
    use quickcheck_macros::quickcheck;
-
    use std::str::FromStr;
-

-
    #[quickcheck]
-
    fn prop_encode_decode(input: PublicKey) {
-
        let encoded = input.to_string();
-
        let decoded = PublicKey::from_str(&encoded).unwrap();
-

-
        assert_eq!(input, decoded);
-
    }
-

-
    #[test]
-
    fn test_encode_decode() {
-
        let input = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
-
        let key = PublicKey::from_str(input).unwrap();
-

-
        assert_eq!(key.to_string(), input);
-
    }
-
}
modified 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::*;

modified radicle/src/identity/project.rs
@@ -488,13 +488,14 @@ impl Identity<Untrusted> {

#[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;
modified 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;
modified 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() {
modified 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<Self> {
-
        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)
deleted radicle/src/ssh.rs
@@ -1,462 +0,0 @@
-
pub mod agent;
-

-
use std::io;
-

-
use thiserror::Error;
-

-
use radicle_ssh::encoding;
-
use radicle_ssh::encoding::Encodable;
-
use radicle_ssh::encoding::Encoding;
-
use radicle_ssh::encoding::Reader;
-

-
use crate::crypto;
-
use crate::crypto::PublicKey;
-

-
pub mod fmt {
-
    use radicle_ssh::encoding::Encoding as _;
-

-
    use crate::crypto::PublicKey;
-

-
    /// Get the SSH long key from a public key.
-
    /// This is the output of `ssh-add -L`.
-
    pub fn key(key: &PublicKey) -> String {
-
        let mut buf = Vec::new();
-

-
        buf.extend_ssh_string(b"ssh-ed25519");
-
        buf.extend_ssh_string(key.as_ref());
-

-
        base64::encode_config(buf, base64::STANDARD_NO_PAD)
-
    }
-

-
    /// Get the SSH key fingerprint from a public key.
-
    /// This is the output of `ssh-add -l`.
-
    pub fn fingerprint(key: &PublicKey) -> String {
-
        use sha2::Digest;
-

-
        let mut buf = Vec::new();
-

-
        buf.extend_ssh_string(b"ssh-ed25519");
-
        buf.extend_ssh_string(key.as_ref());
-

-
        let sha = sha2::Sha256::digest(&buf).to_vec();
-
        let encoded = base64::encode_config(sha, base64::STANDARD_NO_PAD);
-

-
        format!("SHA256:{}", encoded)
-
    }
-

-
    #[cfg(test)]
-
    mod test {
-
        use std::str::FromStr;
-

-
        use super::*;
-
        use crate::crypto::PublicKey;
-

-
        #[test]
-
        fn test_key() {
-
            let pk =
-
                PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
-

-
            assert_eq!(
-
                key(&pk),
-
                "AAAAC3NzaC1lZDI1NTE5AAAAINDoXIrhcnRjnLGUXUFdxhkuy08lkTOwrj2IoGsEX6+Q"
-
            );
-
        }
-

-
        #[test]
-
        fn test_fingerprint() {
-
            let pk =
-
                PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
-
            assert_eq!(
-
                fingerprint(&pk),
-
                "SHA256:gE/Ty4fuXzww49lcnNe9/GI0L7xSEQdFp/v9tOjFwB4"
-
            );
-
        }
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
pub enum SignatureError {
-
    #[error(transparent)]
-
    Invalid(#[from] crypto::Error),
-
    #[error(transparent)]
-
    Encoding(#[from] encoding::Error),
-
    #[error("unknown algorithm '{0}'")]
-
    UnknownAlgorithm(String),
-
}
-

-
impl Encodable for crypto::Signature {
-
    type Error = SignatureError;
-

-
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
-
        let buf = r.read_string()?;
-
        let mut inner_strs = buf.reader(0);
-

-
        let sig_type = inner_strs.read_string()?;
-
        if sig_type != b"ssh-ed25519" {
-
            return Err(SignatureError::UnknownAlgorithm(
-
                String::from_utf8_lossy(sig_type).to_string(),
-
            ));
-
        }
-
        let sig = crypto::Signature::try_from(inner_strs.read_string()?)?;
-

-
        Ok(sig)
-
    }
-

-
    fn write<E: Encoding>(&self, buf: &mut E) {
-
        let mut inner_strs = Vec::new();
-
        inner_strs.extend_ssh_string(b"ssh-ed25519");
-
        inner_strs.extend_ssh_string(self.as_ref());
-
        buf.extend_ssh_string(&inner_strs);
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
pub enum PublicKeyError {
-
    #[error(transparent)]
-
    Invalid(#[from] crypto::Error),
-
    #[error(transparent)]
-
    Encoding(#[from] encoding::Error),
-
    #[error("unknown algorithm '{0}'")]
-
    UnknownAlgorithm(String),
-
}
-

-
impl Encodable for PublicKey {
-
    type Error = PublicKeyError;
-

-
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
-
        match r.read_string()? {
-
            b"ssh-ed25519" => {
-
                let s = r.read_string()?;
-
                let p = PublicKey::try_from(s)?;
-

-
                Ok(p)
-
            }
-
            v => Err(PublicKeyError::UnknownAlgorithm(
-
                String::from_utf8_lossy(v).to_string(),
-
            )),
-
        }
-
    }
-

-
    fn write<E: Encoding>(&self, w: &mut E) {
-
        let mut str_w: Vec<u8> = Vec::<u8>::new();
-
        str_w.extend_ssh_string(b"ssh-ed25519");
-
        str_w.extend_ssh_string(&self[..]);
-
        w.extend_ssh_string(&str_w)
-
    }
-
}
-

-
// FIXME: Should zeroize, or we should be creating our own type
-
// in `crypto`.
-
struct SecretKey(crypto::SecretKey);
-

-
impl From<crypto::SecretKey> for SecretKey {
-
    fn from(other: crypto::SecretKey) -> Self {
-
        Self(other)
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
pub enum SecretKeyError {
-
    #[error(transparent)]
-
    Encoding(#[from] encoding::Error),
-
    #[error(transparent)]
-
    Crypto(#[from] crypto::Error),
-
    #[error(transparent)]
-
    Io(#[from] io::Error),
-
    #[error("unknown algorithm '{0}'")]
-
    UnknownAlgorithm(String),
-
    #[error("public key does not match secret key")]
-
    Mismatch,
-
}
-

-
impl Encodable for SecretKey {
-
    type Error = SecretKeyError;
-

-
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
-
        match r.read_string()? {
-
            b"ssh-ed25519" => {
-
                let public = r.read_string()?;
-
                let pair = r.read_string()?;
-
                let _comment = r.read_string()?;
-
                let key = crypto::SecretKey::from_slice(pair)?;
-

-
                if public != key.public_key().as_ref() {
-
                    return Err(SecretKeyError::Mismatch);
-
                }
-
                Ok(SecretKey(key))
-
            }
-
            s => Err(SecretKeyError::UnknownAlgorithm(
-
                String::from_utf8_lossy(s).to_string(),
-
            )),
-
        }
-
    }
-

-
    fn write<E: Encoding>(&self, buf: &mut E) {
-
        let public = self.0.public_key();
-

-
        buf.extend_ssh_string(b"ssh-ed25519");
-
        buf.extend_ssh_string(public.as_ref());
-
        buf.extend_ssh_string(&*self.0);
-
        buf.extend_ssh_string(b"radicle");
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
pub enum ExtendedSignatureError {
-
    #[error(transparent)]
-
    Base64Encoding(#[from] base64::DecodeError),
-
    #[error("wrong preamble")]
-
    MagicPreamble([u8; 6]),
-
    #[error("missing armored footer")]
-
    MissingFooter,
-
    #[error("missing armored header")]
-
    MissingHeader,
-
    #[error(transparent)]
-
    Encoding(#[from] encoding::Error),
-
    #[error(transparent)]
-
    PublicKey(#[from] PublicKeyError),
-
    #[error(transparent)]
-
    SignatureError(#[from] SignatureError),
-
    #[error("unsupported version '{0}'")]
-
    UnsupportedVersion(u32),
-
}
-

-
/// An SSH signature's decoded format.
-
///
-
/// See <https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig>
-
#[derive(Clone, Debug)]
-
pub struct ExtendedSignature {
-
    version: u32,
-
    public_key: crypto::PublicKey,
-
    /// Unambigious interpretation domain to prevent cross-protocol attacks.
-
    namespace: Vec<u8>,
-
    reserved: Vec<u8>,
-
    /// Hash used for signature. For example 'sha256'.
-
    hash_algorithm: Vec<u8>,
-
    signature: crypto::Signature,
-
}
-

-
impl Encodable for ExtendedSignature {
-
    type Error = ExtendedSignatureError;
-

-
    fn read(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
-
        let sig_version = r.read_u32()?;
-
        if sig_version > 1 {
-
            return Err(ExtendedSignatureError::UnsupportedVersion(sig_version));
-
        }
-
        let mut pk = r.read_string()?.reader(0);
-

-
        Ok(ExtendedSignature {
-
            version: sig_version,
-
            public_key: PublicKey::read(&mut pk)?,
-
            namespace: r.read_string()?.into(),
-
            reserved: r.read_string()?.into(),
-
            hash_algorithm: r.read_string()?.into(),
-
            signature: crypto::Signature::read(r)?,
-
        })
-
    }
-

-
    fn write<E: Encoding>(&self, buf: &mut E) {
-
        buf.extend_u32(self.version);
-
        let _ = &self.public_key.write(buf);
-
        buf.extend_ssh_string(&self.namespace);
-
        buf.extend_ssh_string(&self.reserved);
-
        buf.extend_ssh_string(&self.hash_algorithm);
-
        let _ = &self.signature.write(buf);
-
    }
-
}
-

-
impl ExtendedSignature {
-
    const ARMORED_HEADER: &[u8] = b"-----BEGIN SSH SIGNATURE-----";
-
    const ARMORED_FOOTER: &[u8] = b"-----END SSH SIGNATURE-----";
-
    const ARMORED_WIDTH: usize = 70;
-
    const MAGIC_PREAMBLE: &[u8] = b"SSHSIG";
-

-
    pub fn from_armored(s: &[u8]) -> Result<Self, ExtendedSignatureError> {
-
        let s = s
-
            .strip_prefix(Self::ARMORED_HEADER)
-
            .ok_or(ExtendedSignatureError::MissingHeader)?;
-
        let s = s
-
            .strip_suffix(Self::ARMORED_FOOTER)
-
            .ok_or(ExtendedSignatureError::MissingFooter)?;
-
        let s: Vec<u8> = s.iter().filter(|b| *b != &b'\n').copied().collect();
-

-
        let buf = base64::decode(s)?;
-
        let mut reader = buf.reader(0);
-

-
        let preamble: [u8; 6] = reader.read_bytes()?;
-
        if preamble != Self::MAGIC_PREAMBLE {
-
            return Err(ExtendedSignatureError::MagicPreamble(preamble));
-
        }
-

-
        let sig = ExtendedSignature::read(&mut reader)?;
-
        Ok(sig)
-
    }
-

-
    pub fn to_armored(&self) -> Vec<u8> {
-
        let mut buf = encoding::Buffer::from(Self::MAGIC_PREAMBLE.to_vec());
-
        self.write(&mut buf);
-

-
        let mut armored = Self::ARMORED_HEADER.to_vec();
-
        armored.push(b'\n');
-

-
        let body = base64::encode(buf);
-
        for line in body.as_bytes().chunks(Self::ARMORED_WIDTH) {
-
            armored.extend(line);
-
            armored.push(b'\n');
-
        }
-

-
        armored.extend(Self::ARMORED_FOOTER);
-
        armored
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use std::sync::{Arc, Mutex};
-

-
    use quickcheck_macros::quickcheck;
-
    use zeroize::Zeroizing;
-

-
    use super::{fmt, ExtendedSignature, SecretKey};
-
    use crate::crypto::PublicKey;
-
    use crate::crypto::{self};
-
    use crate::test::arbitrary::ByteArray;
-
    use radicle_ssh::agent::client::{AgentClient, ClientStream, Error};
-
    use radicle_ssh::encoding::*;
-

-
    #[derive(Clone, Default)]
-
    struct DummyStream {
-
        incoming: Arc<Mutex<Zeroizing<Vec<u8>>>>,
-
    }
-

-
    impl ClientStream for DummyStream {
-
        fn connect_socket<P>(_path: P) -> Result<AgentClient<Self>, Error>
-
        where
-
            P: AsRef<std::path::Path> + Send,
-
        {
-
            panic!("This function should never be called!")
-
        }
-

-
        fn read_response(&mut self, buf: &mut Zeroizing<Vec<u8>>) -> Result<(), Error> {
-
            *self.incoming.lock().unwrap() = buf.clone();
-

-
            Ok(())
-
        }
-
    }
-

-
    #[quickcheck]
-
    fn prop_encode_decode_sk(input: ByteArray<64>) {
-
        let mut buf = Buffer::default();
-
        let sk = crypto::SecretKey::new(input.into_inner());
-
        SecretKey(sk).write(&mut buf);
-

-
        let mut cursor = buf.reader(0);
-
        let output = SecretKey::read(&mut cursor).unwrap();
-

-
        assert_eq!(sk, output.0);
-
    }
-

-
    #[test]
-
    fn test_agent_encoding_remove() {
-
        use std::str::FromStr;
-

-
        let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
-
        let expected = [
-
            0, 0, 0, 56, // Message length
-
            18, // Message type (remove identity)
-
            0, 0, 0, 51, // Key blob length
-
            0, 0, 0, 11, // Key type length
-
            115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type
-
            0, 0, 0, 32, // Key length
-
            208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79,
-
            37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key
-
        ];
-

-
        let stream = DummyStream::default();
-
        let mut agent = AgentClient::connect(stream.clone());
-

-
        agent.remove_identity(&pk).unwrap();
-

-
        assert_eq!(
-
            stream.incoming.lock().unwrap().as_slice(),
-
            expected.as_slice()
-
        );
-
    }
-

-
    #[test]
-
    fn test_agent_encoding_sign() {
-
        use std::str::FromStr;
-

-
        let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
-
        let expected = [
-
            0, 0, 0, 73, // Message length
-
            13, // Message type (sign request)
-
            0, 0, 0, 51, // Key blob length
-
            0, 0, 0, 11, // Key type length
-
            115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type
-
            0, 0, 0, 32, // Public key
-
            208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79,
-
            37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key
-
            0, 0, 0, 9, // Length of data to sign
-
            1, 2, 3, 4, 5, 6, 7, 8, 9, // Data to sign
-
            0, 0, 0, 0, // Signature flags
-
        ];
-

-
        let stream = DummyStream::default();
-
        let mut agent = AgentClient::connect(stream.clone());
-
        let data: Zeroizing<Vec<u8>> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9].into();
-

-
        agent.sign_request(&pk, data).ok();
-

-
        assert_eq!(
-
            stream.incoming.lock().unwrap().as_slice(),
-
            expected.as_slice()
-
        );
-
    }
-

-
    #[test]
-
    fn test_signature_encode_decode() {
-
        let armored: &[u8] = b"-----BEGIN SSH SIGNATURE-----
-
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
-
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
-
AAAAQI84aPZsXxlQigpy1/Y/iJSmHSS//CIgvqvUMQIb/TM2vhCKruduH0cK02k9G8wOI+
-
EUMf2bSDyxbJyZThOEiAs=
-
-----END SSH SIGNATURE-----";
-

-
        let public_key = "AAAAC3NzaC1lZDI1NTE5AAAAIL460KIEccS4881p7PPpiiQBsxF+H5tgC6De6crw9rbU";
-
        let signature = ExtendedSignature::from_armored(armored).unwrap();
-

-
        assert_eq!(signature.version, 1);
-
        assert_eq!(fmt::key(&signature.public_key), public_key);
-
        assert_eq!(
-
            String::from_utf8(armored.to_vec()),
-
            String::from_utf8(signature.to_armored()),
-
            "signature should remain unaltered after decoding"
-
        );
-
    }
-

-
    #[test]
-
    fn test_signature_verify() {
-
        let seed = crypto::Seed::new([1; 32]);
-
        let pair = crypto::KeyPair::from_seed(seed);
-
        let message = &[0xff];
-
        let sig = pair.sk.sign(message, None);
-
        let esig = ExtendedSignature {
-
            version: 1,
-
            public_key: pair.pk.into(),
-
            signature: sig.into(),
-
            hash_algorithm: vec![],
-
            namespace: vec![],
-
            reserved: vec![],
-
        };
-

-
        let armored = esig.to_armored();
-
        let unarmored = ExtendedSignature::from_armored(&armored).unwrap();
-

-
        unarmored
-
            .public_key
-
            .verify(message, &unarmored.signature)
-
            .unwrap();
-
    }
-
}
deleted radicle/src/ssh/agent.rs
@@ -1,21 +0,0 @@
-
use radicle_ssh::agent::client::AgentClient;
-
use radicle_ssh::{self as ssh, agent::client::ClientStream};
-

-
use crate::crypto;
-
use crate::ssh::SecretKey;
-

-
#[cfg(not(unix))]
-
use std::net::TcpStream as Stream;
-
#[cfg(unix)]
-
use std::os::unix::net::UnixStream as Stream;
-

-
pub fn connect() -> Result<AgentClient<Stream>, ssh::agent::client::Error> {
-
    Stream::connect_env()
-
}
-

-
pub fn register(key: &crypto::SecretKey) -> Result<(), ssh::agent::client::Error> {
-
    let mut agent = self::connect()?;
-
    agent.add_identity(&SecretKey::from(*key), &[])?;
-

-
    Ok(())
-
}
modified 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;
modified 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() {
modified 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;
modified radicle/src/test.rs
@@ -2,5 +2,4 @@
pub mod arbitrary;
pub mod assert;
pub mod fixtures;
-
pub mod signer;
pub mod storage;
modified 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<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
@@ -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)
-
    }
-
}
deleted radicle/src/test/signer.rs
@@ -1,65 +0,0 @@
-
use crate::crypto::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer};
-

-
#[derive(Debug, Clone)]
-
pub struct MockSigner {
-
    pk: PublicKey,
-
    sk: SecretKey,
-
}
-

-
impl MockSigner {
-
    pub fn new(rng: &mut fastrand::Rng) -> Self {
-
        let mut bytes: [u8; 32] = [0; 32];
-

-
        for byte in &mut bytes {
-
            *byte = rng.u8(..);
-
        }
-
        let seed = Seed::new(bytes);
-
        let keypair = KeyPair::from_seed(seed);
-

-
        Self::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,
-
        }
-
    }
-
}
-

-
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)
-
    }
-
}
-

-
impl Signer for MockSigner {
-
    fn public_key(&self) -> &PublicKey {
-
        &self.pk
-
    }
-

-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.sk.sign(msg, None).into()
-
    }
-
}