crypto: Use `ssh-key` crate for signatures
This commit is contained in:
parent
7e1b2b6222
commit
e584bd6c1c
|
|
@ -1919,7 +1919,6 @@ name = "radicle-crypto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"amplify",
|
"amplify",
|
||||||
"base64",
|
|
||||||
"cyphernet",
|
"cyphernet",
|
||||||
"ec25519",
|
"ec25519",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ use git_trailers::OwnedTrailer;
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
||||||
use crate::history::entry::Timestamp;
|
use crate::history::entry::Timestamp;
|
||||||
|
use crate::signatures;
|
||||||
use crate::{
|
use crate::{
|
||||||
change::{self, store, Change},
|
change::{self, store, Change},
|
||||||
history::entry,
|
history::entry,
|
||||||
signatures::{Signature, Signatures},
|
signatures::{ExtendedSignature, Signatures},
|
||||||
trailers,
|
trailers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -40,6 +41,8 @@ pub mod error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Signer(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
|
Signer(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
Signatures(#[from] Signatures),
|
||||||
|
#[error(transparent)]
|
||||||
Utf8(#[from] Utf8Error),
|
Utf8(#[from] Utf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +87,7 @@ impl change::Storage for git2::Repository {
|
||||||
|
|
||||||
type ObjectId = Oid;
|
type ObjectId = Oid;
|
||||||
type Resource = Oid;
|
type Resource = Oid;
|
||||||
type Signatures = Signature;
|
type Signatures = ExtendedSignature;
|
||||||
|
|
||||||
fn store<Signer>(
|
fn store<Signer>(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -113,7 +116,7 @@ impl change::Storage for git2::Repository {
|
||||||
let signature = {
|
let signature = {
|
||||||
let sig = signer.sign(revision.as_bytes());
|
let sig = signer.sign(revision.as_bytes());
|
||||||
let key = signer.public_key();
|
let key = signer.public_key();
|
||||||
Signature::from((*key, sig))
|
ExtendedSignature::new(*key, sig)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (id, timestamp) = write_commit(self, resource, tips, message, signature.clone(), tree)?;
|
let (id, timestamp) = write_commit(self, resource, tips, message, signature.clone(), tree)?;
|
||||||
|
|
@ -135,7 +138,7 @@ impl change::Storage for git2::Repository {
|
||||||
let mut signatures = Signatures::try_from(&commit)?
|
let mut signatures = Signatures::try_from(&commit)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let Some(signature) = signatures.pop() else {
|
let Some((key, sig)) = signatures.pop() else {
|
||||||
return Err(error::Load::ChangeNotSigned(id));
|
return Err(error::Load::ChangeNotSigned(id));
|
||||||
};
|
};
|
||||||
if !signatures.is_empty() {
|
if !signatures.is_empty() {
|
||||||
|
|
@ -149,7 +152,7 @@ impl change::Storage for git2::Repository {
|
||||||
Ok(Change {
|
Ok(Change {
|
||||||
id,
|
id,
|
||||||
revision: tree.id().into(),
|
revision: tree.id().into(),
|
||||||
signature: signature.into(),
|
signature: ExtendedSignature::new(key, sig),
|
||||||
resource,
|
resource,
|
||||||
manifest,
|
manifest,
|
||||||
contents,
|
contents,
|
||||||
|
|
@ -220,7 +223,7 @@ fn write_commit<O>(
|
||||||
resource: O,
|
resource: O,
|
||||||
tips: Vec<O>,
|
tips: Vec<O>,
|
||||||
message: String,
|
message: String,
|
||||||
signature: Signature,
|
signature: ExtendedSignature,
|
||||||
tree: git2::Tree,
|
tree: git2::Tree,
|
||||||
) -> Result<(Oid, Timestamp), error::Create>
|
) -> Result<(Oid, Timestamp), error::Create>
|
||||||
where
|
where
|
||||||
|
|
@ -240,7 +243,10 @@ where
|
||||||
let mut headers = commit::Headers::new();
|
let mut headers = commit::Headers::new();
|
||||||
headers.push(
|
headers.push(
|
||||||
"gpgsig",
|
"gpgsig",
|
||||||
&String::from_utf8(crypto::ssh::ExtendedSignature::from(signature).to_armored())?,
|
signature
|
||||||
|
.to_pem()
|
||||||
|
.map_err(signatures::error::Signatures::from)?
|
||||||
|
.as_str(),
|
||||||
);
|
);
|
||||||
let author = commit::Author::try_from(&author)?;
|
let author = commit::Author::try_from(&author)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ use git_ext::Oid;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub use store::{Storage, Template};
|
pub use store::{Storage, Template};
|
||||||
|
|
||||||
use crate::signatures::Signature;
|
use crate::signatures::ExtendedSignature;
|
||||||
|
|
||||||
/// A single change in the change graph. The layout of changes in the repository
|
/// A single change in the change graph. The layout of changes in the repository
|
||||||
/// is specified in the RFC (docs/rfc/0662-collaborative-objects.adoc)
|
/// is specified in the RFC (docs/rfc/0662-collaborative-objects.adoc)
|
||||||
/// under "Change Commits".
|
/// under "Change Commits".
|
||||||
pub type Change = store::Change<Oid, Oid, Signature>;
|
pub type Change = store::Change<Oid, Oid, ExtendedSignature>;
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, Id> Change<R, Id, signatures::Signature>
|
impl<R, Id> Change<R, Id, signatures::ExtendedSignature>
|
||||||
where
|
where
|
||||||
Id: AsRef<[u8]>,
|
Id: AsRef<[u8]>,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use git_ext::Oid;
|
||||||
use radicle_dag::{Dag, Node};
|
use radicle_dag::{Dag, Node};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
change, object, signatures::Signature, Change, CollaborativeObject, ObjectId, TypeName,
|
change, object, signatures::ExtendedSignature, Change, CollaborativeObject, ObjectId, TypeName,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod evaluation;
|
mod evaluation;
|
||||||
|
|
@ -31,7 +31,7 @@ impl ChangeGraph {
|
||||||
oid: &ObjectId,
|
oid: &ObjectId,
|
||||||
) -> Option<ChangeGraph>
|
) -> Option<ChangeGraph>
|
||||||
where
|
where
|
||||||
S: change::Storage<ObjectId = Oid, Resource = Oid, Signatures = Signature>,
|
S: change::Storage<ObjectId = Oid, Resource = Oid, Signatures = ExtendedSignature>,
|
||||||
{
|
{
|
||||||
log::info!("loading object '{}' '{}'", typename, oid);
|
log::info!("loading object '{}' '{}'", typename, oid);
|
||||||
let mut builder = GraphBuilder::default();
|
let mut builder = GraphBuilder::default();
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ pub use history::{Contents, Entry, History};
|
||||||
mod pruning_fold;
|
mod pruning_fold;
|
||||||
|
|
||||||
pub mod signatures;
|
pub mod signatures;
|
||||||
use signatures::Signature;
|
use signatures::ExtendedSignature;
|
||||||
|
|
||||||
pub mod type_name;
|
pub mod type_name;
|
||||||
pub use type_name::TypeName;
|
pub use type_name::TypeName;
|
||||||
|
|
@ -135,7 +135,7 @@ where
|
||||||
LoadError = git::change::error::Load,
|
LoadError = git::change::error::Load,
|
||||||
ObjectId = git_ext::Oid,
|
ObjectId = git_ext::Oid,
|
||||||
Resource = git_ext::Oid,
|
Resource = git_ext::Oid,
|
||||||
Signatures = Signature,
|
Signatures = ExtendedSignature,
|
||||||
>,
|
>,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,45 +10,15 @@ use std::{
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crypto::{ssh::ExtendedSignature, PublicKey};
|
use crypto::{ssh, PublicKey};
|
||||||
use git_commit::{
|
use git_commit::{
|
||||||
Commit,
|
Commit,
|
||||||
Signature::{Pgp, Ssh},
|
Signature::{Pgp, Ssh},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use ssh::ExtendedSignature;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Signature {
|
|
||||||
pub(super) key: PublicKey,
|
|
||||||
pub(super) sig: crypto::Signature,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signature {
|
|
||||||
pub fn verify(&self, payload: &[u8]) -> bool {
|
|
||||||
self.key.verify(payload, &self.sig).is_ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Signature> for ExtendedSignature {
|
|
||||||
fn from(sig: Signature) -> Self {
|
|
||||||
Self::new(sig.key, sig.sig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ExtendedSignature> for Signature {
|
|
||||||
fn from(ex: ExtendedSignature) -> Self {
|
|
||||||
let (key, sig) = ex.into();
|
|
||||||
Self { key, sig }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(PublicKey, crypto::Signature)> for Signature {
|
|
||||||
fn from((key, sig): (PublicKey, crypto::Signature)) -> Self {
|
|
||||||
Self { key, sig }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME(kim): This should really be a HashMap with a no-op Hasher -- PublicKey
|
// FIXME(kim): This should really be a HashMap with a no-op Hasher -- PublicKey
|
||||||
// collisions are catastrophic
|
// collisions are catastrophic
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
@ -68,8 +38,8 @@ impl DerefMut for Signatures {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Signature> for Signatures {
|
impl From<ExtendedSignature> for Signatures {
|
||||||
fn from(Signature { key, sig }: Signature) -> Self {
|
fn from(ExtendedSignature { key, sig }: ExtendedSignature) -> Self {
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
map.insert(key, sig);
|
map.insert(key, sig);
|
||||||
map.into()
|
map.into()
|
||||||
|
|
@ -98,13 +68,13 @@ impl TryFrom<&Commit> for Signatures {
|
||||||
match signature {
|
match signature {
|
||||||
// Skip PGP signatures
|
// Skip PGP signatures
|
||||||
Pgp(_) => None,
|
Pgp(_) => None,
|
||||||
Ssh(armored) => Some(
|
Ssh(pem) => Some(
|
||||||
ExtendedSignature::from_armored(armored.as_bytes())
|
ExtendedSignature::from_pem(pem.as_bytes())
|
||||||
.map_err(error::Signatures::from),
|
.map_err(error::Signatures::from),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(|ex| ex.map(|ex| ex.into()))
|
.map(|r| r.map(|es| (es.key, es.sig)))
|
||||||
.collect::<Result<_, _>>()
|
.collect::<Result<_, _>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,12 +97,12 @@ impl IntoIterator for Signatures {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extend<Signature> for Signatures {
|
impl Extend<ExtendedSignature> for Signatures {
|
||||||
fn extend<T>(&mut self, iter: T)
|
fn extend<T>(&mut self, iter: T)
|
||||||
where
|
where
|
||||||
T: IntoIterator<Item = Signature>,
|
T: IntoIterator<Item = ExtendedSignature>,
|
||||||
{
|
{
|
||||||
for Signature { key, sig } in iter {
|
for ExtendedSignature { key, sig } in iter {
|
||||||
self.insert(key, sig);
|
self.insert(key, sig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test = ["fastrand", "qcheck"]
|
test = ["fastrand", "qcheck"]
|
||||||
ssh = ["base64", "radicle-ssh", "ssh-key"]
|
ssh = ["radicle-ssh", "ssh-key"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
amplify = { version = "4.0.0-beta.4" }
|
amplify = { version = "4.0.0-beta.4" }
|
||||||
|
|
@ -34,7 +34,7 @@ optional = true
|
||||||
[dependencies.ssh-key]
|
[dependencies.ssh-key]
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["std", "encryption", "rand_core", "getrandom"]
|
features = ["std", "encryption", "getrandom"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.qcheck]
|
[dependencies.qcheck]
|
||||||
|
|
@ -48,10 +48,6 @@ version = "0"
|
||||||
default-features = false
|
default-features = false
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.base64]
|
|
||||||
version = "0.13"
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fastrand = { version = "1.8.0", default-features = false }
|
fastrand = { version = "1.8.0", default-features = false }
|
||||||
qcheck-macros = { version = "1", default-features = false }
|
qcheck-macros = { version = "1", default-features = false }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,65 @@ use crate::PublicKey;
|
||||||
|
|
||||||
pub use keystore::{Keystore, Passphrase};
|
pub use keystore::{Keystore, Passphrase};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ExtendedSignatureError {
|
||||||
|
#[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 ExtendedSignature {
|
||||||
|
/// Create a new extended signature.
|
||||||
|
pub fn new(public_key: crypto::PublicKey, signature: crypto::Signature) -> Self {
|
||||||
|
Self {
|
||||||
|
key: public_key,
|
||||||
|
sig: signature,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to OpenSSH standard PEM format.
|
||||||
|
pub fn to_pem(&self) -> Result<String, ExtendedSignatureError> {
|
||||||
|
ssh_key::SshSig::new(
|
||||||
|
ssh_key::public::KeyData::from(ssh_key::public::Ed25519PublicKey(**self.key)),
|
||||||
|
String::from("radicle"),
|
||||||
|
ssh_key::HashAlg::Sha256,
|
||||||
|
ssh_key::Signature::new(ssh_key::Algorithm::Ed25519, **self.sig)?,
|
||||||
|
)?
|
||||||
|
.to_pem(ssh_key::LineEnding::default())
|
||||||
|
.map_err(ExtendedSignatureError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from OpenSSH PEM format.
|
||||||
|
pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self, ExtendedSignatureError> {
|
||||||
|
let sig = ssh_key::SshSig::from_pem(pem)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
key: crypto::PublicKey::from(
|
||||||
|
sig.public_key()
|
||||||
|
.ed25519()
|
||||||
|
.ok_or(ExtendedSignatureError::UnsupportedAlgorithm)?
|
||||||
|
.0,
|
||||||
|
),
|
||||||
|
sig: crypto::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 {
|
pub mod fmt {
|
||||||
use crate::PublicKey;
|
use crate::PublicKey;
|
||||||
|
|
||||||
|
|
@ -179,139 +238,12 @@ impl Encodable for crypto::SecretKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 From<ExtendedSignature> for (crypto::PublicKey, crypto::Signature) {
|
|
||||||
fn from(ex: ExtendedSignature) -> Self {
|
|
||||||
(ex.public_key, ex.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 new(public_key: crypto::PublicKey, signature: crypto::Signature) -> Self {
|
|
||||||
Self {
|
|
||||||
version: 1,
|
|
||||||
public_key,
|
|
||||||
namespace: b"radicle".to_vec(),
|
|
||||||
reserved: b"".to_vec(),
|
|
||||||
hash_algorithm: b"sha256".to_vec(),
|
|
||||||
signature,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtendedSignature::read(&mut reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use qcheck_macros::quickcheck;
|
use qcheck_macros::quickcheck;
|
||||||
|
|
||||||
use super::{fmt, ExtendedSignature};
|
|
||||||
use crate as crypto;
|
use crate as crypto;
|
||||||
use crate::{PublicKey, SecretKey};
|
use crate::{PublicKey, SecretKey};
|
||||||
use radicle_ssh::agent::client::{AgentClient, ClientStream, Error};
|
use radicle_ssh::agent::client::{AgentClient, ClientStream, Error};
|
||||||
|
|
@ -406,50 +338,4 @@ mod test {
|
||||||
expected.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 =
|
|
||||||
"ssh-ed25519 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue