radicle-heartwood-lfs/radicle-crypto/src/ssh.rs

470 lines
14 KiB
Rust

pub mod agent;
pub mod keystore;
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 use keystore::{Keystore, Passphrase};
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)
}
}
#[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 crypto::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::try_from(pair)?;
if public != key.public_key().as_ref() {
return Err(SecretKeyError::Mismatch);
}
Ok(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.as_ref());
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 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)]
mod test {
use std::sync::{Arc, Mutex};
use qcheck_macros::quickcheck;
use super::{fmt, ExtendedSignature};
use crate as crypto;
use crate::{PublicKey, SecretKey};
use radicle_ssh::agent::client::{AgentClient, ClientStream, Error};
use radicle_ssh::encoding::*;
#[derive(Clone, Default)]
struct DummyStream {
incoming: Arc<Mutex<Vec<u8>>>,
}
impl ClientStream for DummyStream {
fn connect<P>(_path: P) -> Result<AgentClient<Self>, Error>
where
P: AsRef<std::path::Path> + Send,
{
panic!("This function should never be called!")
}
fn request(&mut self, buf: &[u8]) -> Result<Buffer, Error> {
*self.incoming.lock().unwrap() = buf.to_vec();
Ok(Buffer::default())
}
}
#[quickcheck]
fn prop_encode_decode_sk(input: [u8; 64]) {
let mut buf = Buffer::default();
let sk = crypto::SecretKey::from(input);
sk.write(&mut buf);
let mut cursor = buf.reader(0);
let output = SecretKey::read(&mut cursor).unwrap();
assert_eq!(sk, output);
}
#[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: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
agent.sign(&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();
}
}