ssh: support encoding/decoding armored signatures
Armored signatures are used by Git for signing commits. Support reading writing these signatures by implementing a new trait to read and write to the SSH formmat. Signed-off-by: Slack Coder <slackcoder@server.ky>
This commit is contained in:
parent
8d80359c38
commit
c294f2ab5c
|
|
@ -262,7 +262,7 @@ impl<S: ClientStream> AgentClient<S> {
|
|||
// string data
|
||||
// uint32 flags
|
||||
|
||||
let mut pk = Vec::new().into();
|
||||
let mut pk = Buffer::default();
|
||||
let n = public.write(&mut pk);
|
||||
let total = 1 + n + 4 + data.len() + 4;
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ impl<S: ClientStream> AgentClient<S> {
|
|||
where
|
||||
K: Public,
|
||||
{
|
||||
let mut pk = Vec::new().into();
|
||||
let mut pk: Buffer = Vec::new().into();
|
||||
let n = public.write(&mut pk);
|
||||
let total = 1 + n;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@ pub enum Error {
|
|||
IndexOutOfBounds,
|
||||
}
|
||||
|
||||
pub trait Encodable: Sized {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
/// Read from the SSH format.
|
||||
fn read_ssh(reader: &mut Cursor) -> Result<Self, Self::Error>;
|
||||
|
||||
/// Write to the SSH format.
|
||||
fn write_ssh<E: Encoding>(&self, buf: &mut E);
|
||||
}
|
||||
|
||||
/// Encode in the SSH format.
|
||||
pub trait Encoding {
|
||||
/// Push an SSH-encoded string to `self`.
|
||||
|
|
@ -39,6 +49,8 @@ pub trait Encoding {
|
|||
fn extend_ssh_mpint(&mut self, s: &[u8]);
|
||||
/// Push an SSH-encoded list.
|
||||
fn extend_list<'a, I: Iterator<Item = &'a [u8]>>(&mut self, list: I);
|
||||
/// Push an SSH-encoded unsigned 32-bit integer.
|
||||
fn extend_u32(&mut self, u: u32);
|
||||
/// Push an SSH-encoded empty list.
|
||||
fn write_empty_list(&mut self);
|
||||
/// Write the buffer length at the beginning of the buffer.
|
||||
|
|
@ -87,6 +99,12 @@ impl Encoding for Vec<u8> {
|
|||
self.extend(&s[i..]);
|
||||
}
|
||||
|
||||
fn extend_u32(&mut self, s: u32) {
|
||||
let mut buf = [0x0; 4];
|
||||
BigEndian::write_u32(&mut buf, s);
|
||||
self.extend(buf);
|
||||
}
|
||||
|
||||
fn extend_list<'a, I: Iterator<Item = &'a [u8]>>(&mut self, list: I) {
|
||||
let len0 = self.len();
|
||||
self.extend(&[0, 0, 0, 0]);
|
||||
|
|
@ -136,6 +154,10 @@ impl Encoding for Buffer {
|
|||
self.deref_mut().write_empty_list()
|
||||
}
|
||||
|
||||
fn extend_u32(&mut self, s: u32) {
|
||||
self.deref_mut().extend_u32(s);
|
||||
}
|
||||
|
||||
fn write_len(&mut self) {
|
||||
self.deref_mut().write_len()
|
||||
}
|
||||
|
|
@ -208,6 +230,14 @@ impl<'a> Cursor<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_bytes<const S: usize>(&mut self) -> Result<[u8; S], Error> {
|
||||
let mut buf = [0; S];
|
||||
for b in buf.iter_mut() {
|
||||
*b = self.read_byte()?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Read one byte from this reader.
|
||||
pub fn read_mpint(&mut self) -> Result<&'a [u8], Error> {
|
||||
let len = self.read_u32()? as usize;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use std::error::Error;
|
||||
|
||||
use crate::encoding::{Buffer, Cursor};
|
||||
use crate::encoding::{Buffer, Cursor, Encoding};
|
||||
|
||||
/// A public SSH key.
|
||||
pub trait Public: Sized {
|
||||
type Error: Error + Send + Sync + 'static;
|
||||
|
||||
/// Write the public key to the given buffer, in SSH "blob" format.
|
||||
fn write(&self, buf: &mut Buffer) -> usize;
|
||||
fn write<E: Encoding>(&self, buf: &mut E) -> usize;
|
||||
/// Read the public key from the given reader.
|
||||
fn read(reader: &mut Cursor) -> Result<Option<Self>, Self::Error>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
pub mod agent;
|
||||
|
||||
use std::io;
|
||||
use std::mem;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
|
@ -9,7 +8,10 @@ use thiserror::Error;
|
|||
use zeroize::Zeroizing;
|
||||
|
||||
use radicle_ssh::encoding;
|
||||
use radicle_ssh::encoding::Encoding as _;
|
||||
use radicle_ssh::encoding::Encodable;
|
||||
use radicle_ssh::encoding::Encoding;
|
||||
use radicle_ssh::encoding::Reader;
|
||||
use radicle_ssh::key::Public;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::crypto::PublicKey;
|
||||
|
|
@ -75,31 +77,79 @@ pub mod fmt {
|
|||
}
|
||||
}
|
||||
|
||||
#[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_ssh(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_ssh<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 radicle_ssh::key::Public for PublicKey {
|
||||
impl Encodable for PublicKey {
|
||||
type Error = PublicKeyError;
|
||||
|
||||
fn write(&self, buf: &mut Zeroizing<Vec<u8>>) -> usize {
|
||||
let mut n = 0;
|
||||
let typ = b"ssh-ed25519";
|
||||
let size = typ.len() + self.len() + mem::size_of::<u32>() * 2;
|
||||
fn read_ssh(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
|
||||
let buf = r.read_string()?;
|
||||
let mut str_r = buf.reader(0);
|
||||
match str_r.read_string()? {
|
||||
b"ssh-ed25519" => {
|
||||
let s = str_r.read_string()?;
|
||||
let p = PublicKey::try_from(s)?;
|
||||
|
||||
buf.write_u32::<BigEndian>(size as u32)
|
||||
.expect("writing to a vector never fails");
|
||||
|
||||
n += mem::size_of::<u32>(); // The blob size.
|
||||
n += buf.extend_ssh_string(typ);
|
||||
n += buf.extend_ssh_string(&self[..]);
|
||||
n
|
||||
Ok(p)
|
||||
}
|
||||
v => Err(PublicKeyError::UnknownAlgorithm(
|
||||
String::from_utf8_lossy(v).to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_ssh<E: Encoding>(&self, w: &mut E) {
|
||||
_ = self.write(w);
|
||||
}
|
||||
}
|
||||
|
||||
impl Public for PublicKey {
|
||||
type Error = PublicKeyError;
|
||||
|
||||
fn read(r: &mut encoding::Cursor) -> Result<Option<Self>, Self::Error> {
|
||||
match r.read_string()? {
|
||||
b"ssh-ed25519" => {
|
||||
|
|
@ -111,6 +161,13 @@ impl radicle_ssh::key::Public for PublicKey {
|
|||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn write<E: Encoding>(&self, buf: &mut E) -> usize {
|
||||
let mut str_w: Vec<u8> = Vec::<u8>::new();
|
||||
str_w.extend_ssh_string(b"ssh-ed25519");
|
||||
str_w.extend_ssh_string(&self[..]);
|
||||
buf.extend_ssh_string(&str_w)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Should zeroize, or we should be creating our own type
|
||||
|
|
@ -184,6 +241,124 @@ impl radicle_ssh::key::Private for 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("public key encoding")]
|
||||
PublicKeyEncoding,
|
||||
#[error(transparent)]
|
||||
PublicKeyError(#[from] PublicKeyError),
|
||||
#[error("signature encoding")]
|
||||
SignatureEncoding,
|
||||
#[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 {
|
||||
sig_version: u32,
|
||||
publickey: 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_ssh(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
|
||||
let sig_version = r.read_u32()?;
|
||||
if sig_version > 1 {
|
||||
return Err(ExtendedSignatureError::UnsupportedVersion(sig_version));
|
||||
}
|
||||
|
||||
Ok(ExtendedSignature {
|
||||
sig_version,
|
||||
publickey: PublicKey::read_ssh(r)
|
||||
.map_err(|_| ExtendedSignatureError::PublicKeyEncoding)?,
|
||||
namespace: r.read_string()?.into(),
|
||||
reserved: r.read_string()?.into(),
|
||||
hash_algorithm: r.read_string()?.into(),
|
||||
signature: crypto::Signature::read_ssh(r)
|
||||
.map_err(|_| ExtendedSignatureError::PublicKeyEncoding)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_ssh<E: Encoding>(&self, buf: &mut E) {
|
||||
buf.extend_u32(self.sig_version);
|
||||
let _ = &self.publickey.write_ssh(buf);
|
||||
buf.extend_ssh_string(&self.namespace);
|
||||
buf.extend_ssh_string(&self.reserved);
|
||||
buf.extend_ssh_string(&self.hash_algorithm);
|
||||
let _ = &self.signature.write_ssh(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_ssh(&mut reader)?;
|
||||
Ok(sig)
|
||||
}
|
||||
|
||||
pub fn to_armored(&self) -> Vec<u8> {
|
||||
let mut v = encoding::Buffer::default();
|
||||
|
||||
v.extend(Self::MAGIC_PREAMBLE);
|
||||
self.write_ssh(&mut v);
|
||||
|
||||
let mut armored = Self::ARMORED_HEADER.to_vec();
|
||||
armored.push(b'\n');
|
||||
|
||||
let body: Vec<u8> = base64::encode(v).into();
|
||||
for line in body.chunks(Self::ARMORED_WIDTH) {
|
||||
armored.extend(line.to_vec());
|
||||
armored.push(b'\n');
|
||||
}
|
||||
|
||||
armored.extend(Self::ARMORED_FOOTER);
|
||||
|
||||
armored
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -191,7 +366,7 @@ mod test {
|
|||
use quickcheck_macros::quickcheck;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::SecretKey;
|
||||
use super::{ExtendedSignature, SecretKey};
|
||||
use crate::crypto;
|
||||
use crate::crypto::PublicKey;
|
||||
use crate::test::arbitrary::ByteArray;
|
||||
|
|
@ -288,4 +463,22 @@ mod test {
|
|||
expected.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_encode_decode() {
|
||||
let armored: &[u8] = b"-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQJ759x+pFz0z2yM13S/sqeOOSgTE3fhoJG54dotNTk17dQEPKnH4S4N5jjA+pxM1mb
|
||||
oejZ0WJ0cQtBjWZ7JEBQM=
|
||||
-----END SSH SIGNATURE-----";
|
||||
|
||||
let signature = ExtendedSignature::from_armored(armored).unwrap();
|
||||
assert_eq!(1, signature.sig_version);
|
||||
assert_eq!(
|
||||
String::from_utf8(armored.to_vec()),
|
||||
String::from_utf8(signature.to_armored()),
|
||||
"signature should remain unaltered after decoding"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue