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
|
// string data
|
||||||
// uint32 flags
|
// uint32 flags
|
||||||
|
|
||||||
let mut pk = Vec::new().into();
|
let mut pk = Buffer::default();
|
||||||
let n = public.write(&mut pk);
|
let n = public.write(&mut pk);
|
||||||
let total = 1 + n + 4 + data.len() + 4;
|
let total = 1 + n + 4 + data.len() + 4;
|
||||||
|
|
||||||
|
|
@ -296,7 +296,7 @@ impl<S: ClientStream> AgentClient<S> {
|
||||||
where
|
where
|
||||||
K: Public,
|
K: Public,
|
||||||
{
|
{
|
||||||
let mut pk = Vec::new().into();
|
let mut pk: Buffer = Vec::new().into();
|
||||||
let n = public.write(&mut pk);
|
let n = public.write(&mut pk);
|
||||||
let total = 1 + n;
|
let total = 1 + n;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,16 @@ pub enum Error {
|
||||||
IndexOutOfBounds,
|
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.
|
/// Encode in the SSH format.
|
||||||
pub trait Encoding {
|
pub trait Encoding {
|
||||||
/// Push an SSH-encoded string to `self`.
|
/// Push an SSH-encoded string to `self`.
|
||||||
|
|
@ -39,6 +49,8 @@ pub trait Encoding {
|
||||||
fn extend_ssh_mpint(&mut self, s: &[u8]);
|
fn extend_ssh_mpint(&mut self, s: &[u8]);
|
||||||
/// Push an SSH-encoded list.
|
/// Push an SSH-encoded list.
|
||||||
fn extend_list<'a, I: Iterator<Item = &'a [u8]>>(&mut self, list: I);
|
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.
|
/// Push an SSH-encoded empty list.
|
||||||
fn write_empty_list(&mut self);
|
fn write_empty_list(&mut self);
|
||||||
/// Write the buffer length at the beginning of the buffer.
|
/// Write the buffer length at the beginning of the buffer.
|
||||||
|
|
@ -87,6 +99,12 @@ impl Encoding for Vec<u8> {
|
||||||
self.extend(&s[i..]);
|
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) {
|
fn extend_list<'a, I: Iterator<Item = &'a [u8]>>(&mut self, list: I) {
|
||||||
let len0 = self.len();
|
let len0 = self.len();
|
||||||
self.extend(&[0, 0, 0, 0]);
|
self.extend(&[0, 0, 0, 0]);
|
||||||
|
|
@ -136,6 +154,10 @@ impl Encoding for Buffer {
|
||||||
self.deref_mut().write_empty_list()
|
self.deref_mut().write_empty_list()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extend_u32(&mut self, s: u32) {
|
||||||
|
self.deref_mut().extend_u32(s);
|
||||||
|
}
|
||||||
|
|
||||||
fn write_len(&mut self) {
|
fn write_len(&mut self) {
|
||||||
self.deref_mut().write_len()
|
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.
|
/// Read one byte from this reader.
|
||||||
pub fn read_mpint(&mut self) -> Result<&'a [u8], Error> {
|
pub fn read_mpint(&mut self) -> Result<&'a [u8], Error> {
|
||||||
let len = self.read_u32()? as usize;
|
let len = self.read_u32()? as usize;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use crate::encoding::{Buffer, Cursor};
|
use crate::encoding::{Buffer, Cursor, Encoding};
|
||||||
|
|
||||||
/// A public SSH key.
|
/// A public SSH key.
|
||||||
pub trait Public: Sized {
|
pub trait Public: Sized {
|
||||||
type Error: Error + Send + Sync + 'static;
|
type Error: Error + Send + Sync + 'static;
|
||||||
|
|
||||||
/// Write the public key to the given buffer, in SSH "blob" format.
|
/// 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.
|
/// Read the public key from the given reader.
|
||||||
fn read(reader: &mut Cursor) -> Result<Option<Self>, Self::Error>;
|
fn read(reader: &mut Cursor) -> Result<Option<Self>, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::mem;
|
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
use byteorder::{BigEndian, WriteBytesExt};
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
|
|
@ -9,7 +8,10 @@ use thiserror::Error;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use radicle_ssh::encoding;
|
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;
|
||||||
use crate::crypto::PublicKey;
|
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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PublicKeyError {
|
pub enum PublicKeyError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Invalid(#[from] crypto::Error),
|
Invalid(#[from] crypto::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Encoding(#[from] encoding::Error),
|
Encoding(#[from] encoding::Error),
|
||||||
|
#[error("unknown algorithm '{0}'")]
|
||||||
|
UnknownAlgorithm(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl radicle_ssh::key::Public for PublicKey {
|
impl Encodable for PublicKey {
|
||||||
type Error = PublicKeyError;
|
type Error = PublicKeyError;
|
||||||
|
|
||||||
fn write(&self, buf: &mut Zeroizing<Vec<u8>>) -> usize {
|
fn read_ssh(r: &mut encoding::Cursor) -> Result<Self, Self::Error> {
|
||||||
let mut n = 0;
|
let buf = r.read_string()?;
|
||||||
let typ = b"ssh-ed25519";
|
let mut str_r = buf.reader(0);
|
||||||
let size = typ.len() + self.len() + mem::size_of::<u32>() * 2;
|
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)
|
Ok(p)
|
||||||
.expect("writing to a vector never fails");
|
}
|
||||||
|
v => Err(PublicKeyError::UnknownAlgorithm(
|
||||||
n += mem::size_of::<u32>(); // The blob size.
|
String::from_utf8_lossy(v).to_string(),
|
||||||
n += buf.extend_ssh_string(typ);
|
)),
|
||||||
n += buf.extend_ssh_string(&self[..]);
|
}
|
||||||
n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn read(r: &mut encoding::Cursor) -> Result<Option<Self>, Self::Error> {
|
||||||
match r.read_string()? {
|
match r.read_string()? {
|
||||||
b"ssh-ed25519" => {
|
b"ssh-ed25519" => {
|
||||||
|
|
@ -111,6 +161,13 @@ impl radicle_ssh::key::Public for PublicKey {
|
||||||
_ => Ok(None),
|
_ => 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
|
// 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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
@ -191,7 +366,7 @@ mod test {
|
||||||
use quickcheck_macros::quickcheck;
|
use quickcheck_macros::quickcheck;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use super::SecretKey;
|
use super::{ExtendedSignature, SecretKey};
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::crypto::PublicKey;
|
use crate::crypto::PublicKey;
|
||||||
use crate::test::arbitrary::ByteArray;
|
use crate::test::arbitrary::ByteArray;
|
||||||
|
|
@ -288,4 +463,22 @@ mod test {
|
||||||
expected.as_slice()
|
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