Files
simple_crypto/src/ssh/mod.rs

428 lines
19 KiB
Rust

//! Most of the routines you want are exported from this module as functions,
//! not as structs, macros, enums, or what have you. In particular, you
//! probably want the `decode` or `encode` functions, or one of the functions
//! that `load`s data from disk or `write`s it. Here's some example code
//! to get you started, using a generated ED25519 key for fun:
//!
//! ```rust
//! use simple_crypto::ed25519::ED25519KeyPair;
//! use simple_crypto::ssh::*;
//!
//! // Generate a new ED25519 key
//! let mut rng = rand::rngs::OsRng::new().unwrap();
//! let kp = ED25519KeyPair::generate(&mut rng);
//!
//! // Now that we have it, we can encode it as a handy ASCII string in memory,
//! // using a totally fake email address for fun:
//! let ascii_rep = encode_ssh(&kp, "fake@email.addr").expect("Encode failure!");
//!
//! // As usual, we should be able to decode anything we encode, and the
//! // keys should match:
//! let (kp2, addr2) = decode_ssh(&ascii_rep).expect("Decode failure!");
//! assert_eq!(kp, kp2);
//! assert_eq!(&addr2, "fake@email.addr");
//!
//! // If you want to write this to a file, you can just do so directly:
//! write_ssh_keyfile("test.ed25519", &kp, "fake@email.addr").expect("write error");
//! // And then load it back:
//! let (kp3, addr3) = load_ssh_keyfile("test.ed25519").expect("load error");
//! // And, of course, it should be the same.
//! assert_eq!(kp, kp3);
//! assert_eq!(addr2, addr3);
//! ```
mod dsa;
mod ecdsa;
mod ed25519;
mod errors;
pub mod frame;
mod rsa;
pub use self::errors::{SSHKeyParseError,SSHKeyRenderError};
use base64::decode;
use self::frame::*;
use std::fs::File;
use std::io::{Cursor,Read,Write};
use std::path::Path;
use super::KeyPair;
/// A trait defining keys that can be parsed / rendered by this library. Note
/// that you probably don't want to use these routines directly; they're mostly
/// used by the internal functions. Perhaps the only reason to use them is to
/// implement them, because you've got another kind of key you want to parse that
/// isn't already part of the library. (In that case, though ... maybe send a
/// patch?)
pub trait SSHKey: Sized + KeyPair {
/// Return true if the given string is a valid key type identifier for this
/// key type. (i.e., "ssh-ed25519" is the identifier for ED25519, and "dss"
/// and "ssh-dss" are both valid identifiers for DSA keys.)
fn valid_keytype(s: &str) -> bool;
/// Parse the public blob info within an SSH blob. I strongly recommend
/// using the functions in `ssh::frame` for this.
fn parse_ssh_public_info<I: Read>(inp: &mut I) -> Result<Self::Public,SSHKeyParseError>;
/// Parse the private blob info within an SSH blob. I strongly recommend
/// using the functions in `ssh::frame` for this.
fn parse_ssh_private_info<I: Read>(inp: &mut I) -> Result<(Self::Private,String),SSHKeyParseError>;
/// Render the public blob info within an SSH blob. I strongly recommend
/// using the functions in `ssh::frame` for this.
fn render_ssh_public_info<O: Write>(&self, out: &mut O) -> Result<(),SSHKeyRenderError>;
/// Render the private blob info within an SSH blob. I strongly recommend
/// using the functions in `ssh::frame` for this.
fn render_ssh_private_info<O: Write>(&self, out: &mut O) -> Result<(),SSHKeyRenderError>;
}
/// Decode a string containing a private key into the appropriate key type and
/// the comment associated with it, usually an email address or similar.
pub fn decode_ssh<KP: SSHKey>(x: &str) -> Result<(KP, String),SSHKeyParseError>
{
let bytes = parse_ssh_private_key_data(x)?;
let data_size = bytes.len() as u64;
let mut byte_cursor = Cursor::new(bytes);
parse_openssh_header(&mut byte_cursor)?;
let ciphername = parse_openssh_string(&mut byte_cursor)?;
if ciphername != "none" {
return Err(SSHKeyParseError::UnknownKeyCipher(ciphername));
}
let kdfname = parse_openssh_string(&mut byte_cursor)?;
if kdfname != "none" {
return Err(SSHKeyParseError::UnknownKeyCipher(kdfname));
}
let kdfoptions = parse_openssh_buffer(&mut byte_cursor)?;
if kdfoptions.len() > 0 {
return Err(SSHKeyParseError::UnexpectedKDFOptions);
}
let numkeys = parse_openssh_u32(&mut byte_cursor)?;
if numkeys != 1 {
return Err(SSHKeyParseError::InvalidNumberOfKeys(numkeys));
}
let pubkey0 = parse_openssh_buffer(&mut byte_cursor)?;
let privkeys = parse_openssh_buffer(&mut byte_cursor)?;
if byte_cursor.position() < data_size {
return Err(SSHKeyParseError::UnknownTrailingData);
}
let mut pubcursor = Cursor::new(pubkey0);
let public = KP::parse_ssh_public_info(&mut pubcursor)?;
let mut privcursor = Cursor::new(privkeys);
let (private, comment) = KP::parse_ssh_private_info(&mut privcursor)?;
Ok((KP::new(public, private), comment))
}
/// Decode a string containing a public key into an appropriate key type and
/// the comment associated with it, usually an email address or similar.
pub fn decode_ssh_pubkey<KP: SSHKey>(s: &str) -> Result<(KP::Public, String),SSHKeyParseError>
{
let mut splitter = s.split_whitespace();
match (splitter.next(), splitter.next(), splitter.next(), splitter.next()) {
(Some(keytype), Some(keymaterial), Some(comment), None) => {
if !KP::valid_keytype(keytype) {
return Err(SSHKeyParseError::InvalidPublicKeyType);
}
let bytes = decode(keymaterial)?;
let mut byte_cursor = Cursor::new(bytes);
let key = KP::parse_ssh_public_info(&mut byte_cursor)?;
Ok((key, comment.to_string()))
}
_ =>
Err(SSHKeyParseError::BrokenPublicKeyLine)
}
}
/// Load an SSH private key file, returning the appropriate key type and the
/// comment associated with it.
pub fn load_ssh_keyfile<KP,P>(path: P) -> Result<(KP, String),SSHKeyParseError>
where
KP: SSHKey,
P: AsRef<Path>
{
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
decode_ssh(&contents)
}
/// Load all the public keys from a file into memory.
pub fn load_ssh_pubkeys<KP,P>(path: P) -> Result<Vec<(KP::Public, String)>,SSHKeyParseError>
where
KP: SSHKey,
P: AsRef<Path>
{
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let mut result = Vec::new();
for line in contents.lines() {
result.push( decode_ssh_pubkey::<KP>(line)? );
}
Ok(result)
}
/// Encode a supported key into its ASCII SSH format, with the given comment.
pub fn encode_ssh<KP: SSHKey>(x: &KP, comment: &str) -> Result<String,SSHKeyRenderError>
{
let mut pubkeybin = Vec::with_capacity(8192);
let mut privkeybin = Vec::with_capacity(8192);
let mut binary = Vec::with_capacity(16384);
// create the public key bits
x.render_ssh_public_info(&mut pubkeybin)?;
// create the private key bits
render_openssh_u32(&mut privkeybin, 0xDEADBEEF)?; // FIXME: Any reason for this to be random?
render_openssh_u32(&mut privkeybin, 0xDEADBEEF)?; // ditto
x.render_ssh_private_info(&mut privkeybin)?;
render_openssh_string(&mut privkeybin, comment)?;
// add some padding (not quite sure why)
let mut i = comment.len();
while (i % 16) != 0 {
privkeybin.write(&[(i - comment.len() + 1) as u8])?;
i += 1;
}
// render a bunch of the framing stuff
render_openssh_header(&mut binary)?;
render_openssh_string(&mut binary, "none")?; // ciphername
render_openssh_string(&mut binary, "none")?; // kdfname
render_openssh_buffer(&mut binary, &[])?; // kdfoptions
render_openssh_u32(&mut binary, 1)?; // numkeys
render_openssh_buffer(&mut binary, &pubkeybin)?;
render_openssh_buffer(&mut binary, &privkeybin)?;
Ok(render_ssh_private_key_data(&binary))
}
/// Encode a supported key into the given file, with the given comment.
pub fn write_ssh_keyfile<KP,P>(path: P, x: &KP, comment: &str) -> Result<(),SSHKeyRenderError>
where
KP: SSHKey,
P: AsRef<Path>
{
let mut file = File::create(path)?;
let contents = encode_ssh(x, comment)?;
let bytes = contents.into_bytes();
file.write_all(&bytes)?;
file.sync_all()?;
Ok(())
}
#[cfg(test)]
use dsa::{DSAKeyPair,DSAPublicKey,L1024N160};
#[cfg(test)]
use ecdsa::ECDSAPair;
#[cfg(test)]
use ed25519::ED25519KeyPair;
#[cfg(test)]
use rsa::{RSAPair,RSAPublic,SIGNING_HASH_SHA256};
#[cfg(test)]
use sha2::Sha256;
#[cfg(test)]
#[test]
fn dsa_examples() {
let test_files = ["dsa1024-1", "dsa1024-2", "dsa1024-3"];
for file in test_files.iter() {
let path = format!("testdata/ssh/{}",file);
let mkeypair = load_ssh_keyfile(path);
match mkeypair {
Err(e) => assert!(false, format!("reading error: {:?}", e)),
Ok((keypair, comment)) => {
let buffer = [0,1,2,3,4,6,2];
let _ : DSAKeyPair<L1024N160> = keypair;
let sig = keypair.private.sign::<Sha256>(&buffer);
assert!(keypair.public.verify::<Sha256>(&buffer, &sig));
let buffer2 = [0,1,2,3,4,6,5];
assert!(!keypair.public.verify::<Sha256>(&buffer2, &sig));
match encode_ssh(&keypair, &comment) {
Err(e2) => assert!(false, format!("render error: {:?}", e2)),
Ok(encodedstr) => {
match decode_ssh(&encodedstr) {
Err(e3) => assert!(false, format!("reparse error: {:?}", e3)),
Ok((keypair2,comment2)) => {
let _ : DSAKeyPair<L1024N160> = keypair2;
assert_eq!(keypair.public.params.p,keypair2.public.params.p,"failed to reparse key pair (p)");
assert_eq!(keypair.public.params.q,keypair2.public.params.q,"failed to reparse key pair (q)");
assert_eq!(keypair.public.params.g,keypair2.public.params.g,"failed to reparse key pair (g)");
assert_eq!(keypair.private.params.p,keypair2.private.params.p,"failed to reparse key pair (p)");
assert_eq!(keypair.private.params.q,keypair2.private.params.q,"failed to reparse key pair (q)");
assert_eq!(keypair.private.params.g,keypair2.private.params.g,"failed to reparse key pair (g)");
assert_eq!(keypair.public.y,keypair2.public.y,"failed to reparse key pair (y)");
assert_eq!(keypair.private.x,keypair2.private.x,"failed to reparse key pair (x)");
assert_eq!(comment,comment2,"failed to reparse comment");
let ppath = format!("testdata/ssh/{}.pub",file);
match load_ssh_pubkeys::<DSAKeyPair<L1024N160>,String>(ppath) {
Err(e4) => assert!(false, format!("pubkey error: {:?}", e4)),
Ok(pubkeys) => {
let _ : Vec<(DSAPublicKey<L1024N160>,String)> = pubkeys;
for (pubkey, comment3) in pubkeys {
assert_eq!(pubkey.params.p, keypair.public.params.p, "public key check (p)");
assert_eq!(pubkey.params.q, keypair.public.params.q, "public key check (q)");
assert_eq!(pubkey.params.g, keypair.public.params.g, "public key check (g)");
assert_eq!(pubkey.y, keypair.public.y, "public key check (y)");
assert_eq!(comment, comment3, "public key check comment")
}
}
}
}
}
}
}
}
}
}
}
#[cfg(test)]
#[test]
fn rsa_examples() {
let test_files = ["rsa1024-1", "rsa1024-2", "rsa1024-3",
"rsa2048-1", "rsa2048-2", "rsa2048-3",
"rsa3072-1", "rsa3072-2", "rsa3072-3",
"rsa4096-1", "rsa4096-2", "rsa4096-3",
"rsa8192-1", "rsa8192-2", "rsa8192-3"];
for file in test_files.iter() {
let path = format!("testdata/ssh/{}",file);
let mkeypair = load_ssh_keyfile::<RSAPair,String>(path);
match mkeypair {
Err(e) => assert!(false, format!("reading error: {:?}", e)),
Ok((keypair, comment)) => {
let buffer = [0,1,2,3,4,6,2];
let sig = keypair.sign(&SIGNING_HASH_SHA256, &buffer);
assert!(keypair.verify(&SIGNING_HASH_SHA256, &buffer, &sig));
match encode_ssh(&keypair, &comment) {
Err(e2) => assert!(false, format!("render error: {:?}", e2)),
Ok(encodedstr) => {
match decode_ssh(&encodedstr) {
Err(e3) => assert!(false, format!("reparse error: {:?}", e3)),
Ok((keypair2,comment2)) => {
assert_eq!(keypair,keypair2,"failed to reparse key pair");
assert_eq!(comment,comment2,"failed to reparse comment");
let ppath = format!("testdata/ssh/{}.pub",file);
match load_ssh_pubkeys::<RSAPair,String>(ppath) {
Err(e4) => assert!(false, format!("pubkey error: {:?}", e4)),
Ok(pubkeys) => {
let _ : Vec<(RSAPublic,String)> = pubkeys;
for (pubkey, comment3) in pubkeys {
assert_eq!(pubkey, keypair.public(), "public key check");
assert_eq!(comment, comment3, "public key check comment");
}
}
}
}
}
}
}
}
}
}
}
#[cfg(test)]
#[test]
fn ecdsa_examples() {
let test_files = ["ecdsa256-1", "ecdsa256-2", "ecdsa256-3",
"ecdsa384-1", "ecdsa384-2", "ecdsa384-3",
"ecdsa521-1", "ecdsa521-2", "ecdsa521-3"];
for file in test_files.iter() {
let path = format!("testdata/ssh/{}",file);
match load_ssh_keyfile::<ECDSAPair,String>(path) {
Err(e) =>
assert!(false, "SSH ECDSA parse error: {:?}", e),
Ok((keypair,comment)) => {
// first see if this roundtrips
let buffer = vec![0,1,2,4,5,6,9];
match keypair {
ECDSAPair::P192(_,_) =>
assert!(false, "Somehow got a P192 in read test"),
ECDSAPair::P224(_,_) =>
assert!(false, "Somehow got a P224 in read test"),
ECDSAPair::P256(ref pu, ref pr) => {
let sig = pr.sign::<Sha256>(&buffer);
assert!(pu.verify::<Sha256>(&buffer, &sig));
}
ECDSAPair::P384(ref pu, ref pr) => {
let sig = pr.sign::<Sha256>(&buffer);
assert!(pu.verify::<Sha256>(&buffer, &sig));
}
ECDSAPair::P521(ref pu, ref pr) => {
let sig = pr.sign::<Sha256>(&buffer);
assert!(pu.verify::<Sha256>(&buffer, &sig));
}
}
// encode this, parse it again
match encode_ssh(&keypair, &comment) {
Err(e) =>
assert!(false, "SSH ECDSA encoding error: {:?}", e),
Ok(coded) => {
match (decode_ssh(&coded), keypair) {
(Err(e), _) =>
assert!(false, "SSSH ECDSA redecoding error: {:?}", e),
(Ok((ECDSAPair::P256(pu2, pr2), comment2)), ECDSAPair::P256(pu,pr)) => {
assert_eq!(pu, pu2, "public key mismatch");
assert_eq!(pr, pr2, "public key mismatch");
assert_eq!(comment, comment2, "comment mismatch");
}
(Ok((ECDSAPair::P384(pu2, pr2), comment2)), ECDSAPair::P384(pu,pr)) => {
assert_eq!(pu, pu2, "public key mismatch");
assert_eq!(pr, pr2, "public key mismatch");
assert_eq!(comment, comment2, "comment mismatch");
}
(Ok((ECDSAPair::P521(pu2, pr2), comment2)), ECDSAPair::P521(pu,pr)) => {
assert_eq!(pu, pu2, "public key mismatch");
assert_eq!(pr, pr2, "public key mismatch");
assert_eq!(comment, comment2, "comment mismatch");
}
_ =>
assert!(false, "Failed to accurately re-parse key")
}
}
}
}
}
}
}
#[cfg(test)]
#[test]
fn ed25519_examples() {
let test_files = ["ed25519-1", "ed25519-2", "ed25519-3"];
for file in test_files.iter() {
let path = format!("testdata/ssh/{}",file);
match load_ssh_keyfile::<ED25519KeyPair,String>(path) {
Err(e) =>
assert!(false, "SSH ED25519 parse error: {:?}", e),
Ok((keypair,comment)) => {
// first see if this roundtrips
let buffer = vec![0,1,2,4,5,6,9];
let sig = keypair.private.sign(&buffer);
assert!(keypair.public.verify(&buffer, &sig));
match encode_ssh(&keypair, &comment) {
Err(e) =>
assert!(false, "SSH ED25519 encoding error: {:?}", e),
Ok(coded) => {
match decode_ssh(&coded) {
Err(e) =>
assert!(false, "SSSH ECDSA redecoding error: {:?}", e),
Ok((keypair2, comment2)) => {
let _ : ED25519KeyPair = keypair2;
assert_eq!(keypair.public, keypair2.public, "public key mismatch");
assert_eq!(keypair.private, keypair2.private, "public key mismatch");
assert_eq!(comment, comment2, "comment mismatch");
}
}
}
}
}
}
}
}