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:
Slack Coder 2022-10-05 08:20:59 -05:00 committed by Alexis Sellier
parent 8d80359c38
commit c294f2ab5c
No known key found for this signature in database
4 changed files with 242 additions and 19 deletions

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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"
);
}
}