Basic support for SSH key parsing, at least for DSA.
This commit is contained in:
@@ -9,7 +9,17 @@ pub enum SSHKeyParseError
|
|||||||
DecodeError(DecodeError),
|
DecodeError(DecodeError),
|
||||||
IOError(io::Error),
|
IOError(io::Error),
|
||||||
NoBeginBannerFound, NoEndBannerFound,
|
NoBeginBannerFound, NoEndBannerFound,
|
||||||
NoOpenSSHMagicHeader
|
NoOpenSSHMagicHeader,
|
||||||
|
UnknownKeyCipher(String),
|
||||||
|
UnknownKDF(String), UnexpectedKDFOptions,
|
||||||
|
InvalidNumberOfKeys(u32),
|
||||||
|
UnknownTrailingData,
|
||||||
|
UnknownKeyType(String),
|
||||||
|
InvalidPublicKeyMaterial,
|
||||||
|
PrivateKeyCorruption,
|
||||||
|
InconsistentKeyTypes(String,String),
|
||||||
|
InconsistentPublicKeyValue,
|
||||||
|
InvalidPrivateKeyValue
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ASN1DecodeErr> for SSHKeyParseError {
|
impl From<ASN1DecodeErr> for SSHKeyParseError {
|
||||||
@@ -31,8 +41,11 @@ impl From<io::Error> for SSHKeyParseError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum SSHKeyRenderError {
|
pub enum SSHKeyRenderError {
|
||||||
IOError(io::Error),
|
IOError(io::Error),
|
||||||
|
StringTooLong,
|
||||||
|
BufferTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for SSHKeyRenderError {
|
impl From<io::Error> for SSHKeyRenderError {
|
||||||
|
|||||||
120
src/ssh/frame.rs
120
src/ssh/frame.rs
@@ -1,11 +1,12 @@
|
|||||||
use base64::{decode,encode};
|
use base64::{decode,encode};
|
||||||
|
use byteorder::{BigEndian,ReadBytesExt,WriteBytesExt};
|
||||||
use ssh::errors::{SSHKeyParseError,SSHKeyRenderError};
|
use ssh::errors::{SSHKeyParseError,SSHKeyRenderError};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
#[cfg(test)]
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::iter::Iterator;
|
||||||
|
|
||||||
const OPENER: &'static str = "-----BEGIN OPENSSH PRIVATE KEY-----\n";
|
const OPENER: &'static str = "-----BEGIN OPENSSH PRIVATE KEY-----\n";
|
||||||
const CLOSER: &'static str = "-----END OPENSSH PRIVATE KEY-----";
|
const CLOSER: &'static str = "-----END OPENSSH PRIVATE KEY-----";
|
||||||
@@ -47,15 +48,24 @@ pub fn render_ssh_private_key_data(bytes: &[u8]) -> String
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const OPENSSH_MAGIC_HEADER: &'static str = "openssh-key-v1\0";
|
const OPENSSH_MAGIC_HEADER: &'static str = "openssh-key-v1\0";
|
||||||
|
const OPENSSH_MAGIC_HEADER_LEN: usize = 15;
|
||||||
|
|
||||||
pub fn parse_openssh_header(input: &mut Cursor<Vec<u8>>) -> Result<(),SSHKeyParseError>
|
pub fn parse_openssh_header<R: Read>(input: &mut R) -> Result<(),SSHKeyParseError>
|
||||||
{
|
{
|
||||||
let input_header = input.take(OPENSSH_MAGIC_HEADER.len()).bytes();
|
let mut limited_input_header = input.take(OPENSSH_MAGIC_HEADER_LEN as u64);
|
||||||
if input_header.eq(OPENSSH_MAGIC_HEADER.as_bytes().iter()) {
|
let mut header: [u8; OPENSSH_MAGIC_HEADER_LEN] = [0; OPENSSH_MAGIC_HEADER_LEN];
|
||||||
Ok(())
|
|
||||||
} else {
|
assert_eq!(OPENSSH_MAGIC_HEADER.len(), OPENSSH_MAGIC_HEADER_LEN);
|
||||||
Err(SSHKeyParseError::NoOpenSSHMagicHeader)
|
limited_input_header.read_exact(&mut header);
|
||||||
|
|
||||||
|
for (left, right) in OPENSSH_MAGIC_HEADER.bytes().zip(header.iter()) {
|
||||||
|
if left != *right {
|
||||||
|
return Err(SSHKeyParseError::NoOpenSSHMagicHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_openssh_header<O: Write>(output: &mut O) -> Result<(),SSHKeyRenderError>
|
pub fn render_openssh_header<O: Write>(output: &mut O) -> Result<(),SSHKeyRenderError>
|
||||||
@@ -65,16 +75,67 @@ pub fn render_openssh_header<O: Write>(output: &mut O) -> Result<(),SSHKeyRender
|
|||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
pub fn parse_openssh_u32(input: &mut Cursor<Vec<u8>>) -> Result<u32,SSHKeyParseError>
|
pub fn parse_openssh_u32<I: Read>(input: &mut I) -> Result<u32,SSHKeyParseError>
|
||||||
{
|
{
|
||||||
|
let mut limited_input_header = input.take(4);
|
||||||
|
let res = limited_input_header.read_u32::<BigEndian>()?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_openssh_u32<O: Write>(output: &mut O, val: u32) -> Result<(),SSHKeyRenderError>
|
||||||
|
{
|
||||||
|
Ok(output.write_u32::<BigEndian>(val)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
pub fn parse_openssh_string(input: &mut Cursor<Vec<u8>>) -> Result<(),SSHKeyParseError>
|
pub fn parse_openssh_string<I: Read>(input: &mut I) -> Result<String,SSHKeyParseError>
|
||||||
{
|
{
|
||||||
panic!("string")
|
let length = parse_openssh_u32(input)?;
|
||||||
|
println!("len: {:X}", length);
|
||||||
|
let mut limited_input = input.take(length as u64);
|
||||||
|
let mut result = String::new();
|
||||||
|
limited_input.read_to_string(&mut result)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_openssh_string<O: Write>(output: &mut O, v: &str) -> Result<(),SSHKeyRenderError>
|
||||||
|
{
|
||||||
|
let vbytes: Vec<u8> = v.bytes().collect();
|
||||||
|
let len = vbytes.len();
|
||||||
|
|
||||||
|
if len > 0xFFFFFFFF {
|
||||||
|
return Err(SSHKeyRenderError::StringTooLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_openssh_u32(output, vbytes.len() as u32)?;
|
||||||
|
output.write_all(&vbytes)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn parse_openssh_buffer<I: Read>(input: &mut I) -> Result<Vec<u8>,SSHKeyParseError>
|
||||||
|
{
|
||||||
|
let length = parse_openssh_u32(input)?;
|
||||||
|
let mut limited_input = input.take(length as u64);
|
||||||
|
let mut res = Vec::with_capacity(length as usize);
|
||||||
|
limited_input.read_to_end(&mut res);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_openssh_buffer<O: Write>(output: &mut O, b: &[u8]) -> Result<(),SSHKeyRenderError>
|
||||||
|
{
|
||||||
|
if b.len() > 0xFFFFFFFF {
|
||||||
|
return Err(SSHKeyRenderError::BufferTooLarge);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_openssh_u32(output, b.len() as u32)?;
|
||||||
|
if b.len() > 0 {
|
||||||
|
output.write_all(b)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -97,6 +158,42 @@ quickcheck! {
|
|||||||
|
|
||||||
is_ok
|
is_ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn u32s_roundtrip_rp(x: u32) -> bool {
|
||||||
|
let mut buffer = vec![];
|
||||||
|
render_openssh_u32(&mut buffer, x).unwrap();
|
||||||
|
let mut cursor = Cursor::new(buffer);
|
||||||
|
let check = parse_openssh_u32(&mut cursor).unwrap();
|
||||||
|
x == check
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32s_roundtrip_pr(a: u8, b: u8, c: u8, d: u8) -> bool {
|
||||||
|
let block = [a,b,c,d];
|
||||||
|
let mut cursor = Cursor::new(block);
|
||||||
|
let base = parse_openssh_u32(&mut cursor).unwrap();
|
||||||
|
let mut rendered = vec![];
|
||||||
|
render_openssh_u32(&mut rendered, base).unwrap();
|
||||||
|
(block[0] == rendered[0]) &&
|
||||||
|
(block[1] == rendered[1]) &&
|
||||||
|
(block[2] == rendered[2]) &&
|
||||||
|
(block[3] == rendered[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_roundtrip(s: String) -> bool {
|
||||||
|
let mut buffer = vec![];
|
||||||
|
render_openssh_string(&mut buffer, &s).unwrap();
|
||||||
|
let mut cursor = Cursor::new(buffer);
|
||||||
|
let check = parse_openssh_string(&mut cursor).unwrap();
|
||||||
|
s == check
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer(os: Vec<u8>) -> bool {
|
||||||
|
let mut buffer = vec![];
|
||||||
|
render_openssh_buffer(&mut buffer, &os).unwrap();
|
||||||
|
let mut cursor = Cursor::new(buffer);
|
||||||
|
let check = parse_openssh_buffer(&mut cursor).unwrap();
|
||||||
|
os == check
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -130,5 +227,6 @@ fn pregenerated_reencode() {
|
|||||||
fn header_roundtrips() {
|
fn header_roundtrips() {
|
||||||
let mut vec = Vec::new();
|
let mut vec = Vec::new();
|
||||||
assert!(render_openssh_header(&mut vec).is_ok());
|
assert!(render_openssh_header(&mut vec).is_ok());
|
||||||
assert!(parse_openssh_header(&mut vec.iter()).is_ok());
|
let mut cursor = Cursor::new(vec);
|
||||||
|
assert!(parse_openssh_header(&mut cursor).is_ok());
|
||||||
}
|
}
|
||||||
154
src/ssh/mod.rs
154
src/ssh/mod.rs
@@ -3,8 +3,8 @@ mod frame;
|
|||||||
|
|
||||||
pub use self::errors::SSHKeyParseError;
|
pub use self::errors::SSHKeyParseError;
|
||||||
|
|
||||||
use cryptonum::unsigned::U192;
|
use cryptonum::unsigned::*;
|
||||||
use dsa::{DSAPrivKey,L1024N160};
|
use dsa::{DSAKeyPair,DSAParameters,DSAPubKey,DSAPublicKey,DSAPrivKey,DSAPrivateKey,L1024N160};
|
||||||
use self::frame::*;
|
use self::frame::*;
|
||||||
use simple_asn1::from_der;
|
use simple_asn1::from_der;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -32,17 +32,143 @@ pub trait SSHKey: Sized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl SSHKey for DSAPrivKey<L1024N160,U192> {
|
impl SSHKey for DSAKeyPair<L1024N160,U1024,U192> {
|
||||||
fn decode_ssh_private_key(x: &str) -> Result<Self,SSHKeyParseError>
|
fn decode_ssh_private_key(x: &str) -> Result<Self,SSHKeyParseError>
|
||||||
{
|
{
|
||||||
let bytes = parse_ssh_private_key_data(x)?;
|
let bytes = parse_ssh_private_key_data(x)?;
|
||||||
|
let data_size = bytes.len() as u64;
|
||||||
let mut byte_cursor = Cursor::new(bytes);
|
let mut byte_cursor = Cursor::new(bytes);
|
||||||
|
|
||||||
parse_openssh_header(&mut byte_cursor)?;
|
parse_openssh_header(&mut byte_cursor)?;
|
||||||
let ciphername = parse_openssh_string(&mut byte_cursor)?;
|
let ciphername = parse_openssh_string(&mut byte_cursor)?;
|
||||||
//
|
if ciphername != "none" {
|
||||||
println!("bytes: {:?}", bytes);
|
return Err(SSHKeyParseError::UnknownKeyCipher(ciphername));
|
||||||
panic!("decode")
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
// Now that we've sorted out the details at this level,
|
||||||
|
// see if we can decode the public key
|
||||||
|
let mut pubkey_cursor = Cursor::new(pubkey0);
|
||||||
|
let pubkey_type = parse_openssh_string(&mut pubkey_cursor)?;
|
||||||
|
if pubkey_type != "ssh-dss" {
|
||||||
|
return Err(SSHKeyParseError::UnknownKeyType(pubkey_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pbytes = parse_openssh_buffer(&mut pubkey_cursor)?;
|
||||||
|
while pbytes[0] == 0 { pbytes.remove(0); }
|
||||||
|
if pbytes.len() > (1024 / 8) {
|
||||||
|
println!("pbytes.len() = {}", pbytes.len());
|
||||||
|
println!("pbytes = {:?}", pbytes);
|
||||||
|
return Err(SSHKeyParseError::InvalidPublicKeyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut qbytes = parse_openssh_buffer(&mut pubkey_cursor)?;
|
||||||
|
while qbytes[0] == 0 { qbytes.remove(0); }
|
||||||
|
if qbytes.len() > (160 / 8) {
|
||||||
|
println!("qbytes.len() = {}", qbytes.len());
|
||||||
|
return Err(SSHKeyParseError::InvalidPublicKeyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut gbytes = parse_openssh_buffer(&mut pubkey_cursor)?;
|
||||||
|
while gbytes[0] == 0 { gbytes.remove(0); }
|
||||||
|
if gbytes.len() > (1024 / 8) {
|
||||||
|
println!("gbytes.len() = {}", gbytes.len());
|
||||||
|
return Err(SSHKeyParseError::InvalidPublicKeyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ybytes = parse_openssh_buffer(&mut pubkey_cursor)?;
|
||||||
|
while ybytes[0] == 0 { ybytes.remove(0); }
|
||||||
|
if ybytes.len() > (1024 / 8) {
|
||||||
|
println!("ybytes.len() = {}", ybytes.len());
|
||||||
|
return Err(SSHKeyParseError::InvalidPublicKeyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we can turn this into a key
|
||||||
|
let p = U1024::from_bytes(&pbytes);
|
||||||
|
let g = U1024::from_bytes(&gbytes);
|
||||||
|
let q = U192::from_bytes(&qbytes);
|
||||||
|
let params = L1024N160::new(p, g, q);
|
||||||
|
let y = U1024::from_bytes(&ybytes);
|
||||||
|
let pubkey = DSAPubKey::<L1024N160,U1024>::new(params, y);
|
||||||
|
|
||||||
|
// And now we can look at the private key!
|
||||||
|
let mut privkey_cursor = Cursor::new(privkeys);
|
||||||
|
let check1 = parse_openssh_u32(&mut privkey_cursor)?;
|
||||||
|
let check2 = parse_openssh_u32(&mut privkey_cursor)?;
|
||||||
|
if check1 != check2 {
|
||||||
|
return Err(SSHKeyParseError::PrivateKeyCorruption);
|
||||||
|
}
|
||||||
|
|
||||||
|
let privkey_type = parse_openssh_string(&mut privkey_cursor)?;
|
||||||
|
if privkey_type != pubkey_type {
|
||||||
|
return Err(SSHKeyParseError::InconsistentKeyTypes(pubkey_type, privkey_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pdata = parse_openssh_buffer(&mut privkey_cursor)?;
|
||||||
|
while pdata[0] == 0 { pdata.remove(0); }
|
||||||
|
if pdata != pbytes {
|
||||||
|
return Err(SSHKeyParseError::InconsistentPublicKeyValue);
|
||||||
|
}
|
||||||
|
println!("p consistent");
|
||||||
|
let p = U1024::from_bytes(&pdata);
|
||||||
|
|
||||||
|
let mut qdata = parse_openssh_buffer(&mut privkey_cursor)?;
|
||||||
|
while qdata[0] == 0 { qdata.remove(0); }
|
||||||
|
if qdata != qbytes {
|
||||||
|
return Err(SSHKeyParseError::InconsistentPublicKeyValue);
|
||||||
|
}
|
||||||
|
let q = U192::from_bytes(&qdata);
|
||||||
|
println!("q consistent");
|
||||||
|
|
||||||
|
let mut gdata = parse_openssh_buffer(&mut privkey_cursor)?;
|
||||||
|
while gdata[0] == 0 { gdata.remove(0); }
|
||||||
|
if gdata != gbytes {
|
||||||
|
return Err(SSHKeyParseError::InconsistentPublicKeyValue);
|
||||||
|
}
|
||||||
|
let g = U1024::from_bytes(&gdata);
|
||||||
|
println!("g consistent");
|
||||||
|
|
||||||
|
let params = L1024N160::new(p, g, q);
|
||||||
|
|
||||||
|
// We don't need this, but it's good cross-validation
|
||||||
|
let mut ydata = parse_openssh_buffer(&mut privkey_cursor)?;
|
||||||
|
while ydata[0] == 0 { ydata.remove(0); }
|
||||||
|
if ydata != ybytes {
|
||||||
|
return Err(SSHKeyParseError::InconsistentPublicKeyValue);
|
||||||
|
}
|
||||||
|
println!("y consistent");
|
||||||
|
|
||||||
|
// Finally, what we actually
|
||||||
|
let mut xdata = parse_openssh_buffer(&mut privkey_cursor)?;
|
||||||
|
while xdata[0] == 0 { xdata.remove(0); }
|
||||||
|
if xdata.len() > (192 / 8) {
|
||||||
|
// FIXME: Should this test for too small a value?
|
||||||
|
return Err(SSHKeyParseError::InvalidPrivateKeyValue);
|
||||||
|
}
|
||||||
|
let x = U192::from_bytes(&xdata);
|
||||||
|
println!("x: {:X}", x);
|
||||||
|
let privkey = DSAPrivKey::<L1024N160,U192>::new(params, x);
|
||||||
|
let comment = parse_openssh_string(&mut privkey_cursor)?;
|
||||||
|
println!("comment[{}] = {:?}", comment.len(), comment);
|
||||||
|
|
||||||
|
let result = DSAKeyPair{ public: pubkey, private: privkey };
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_ssh_private_key(&self) -> String {
|
fn encode_ssh_private_key(&self) -> String {
|
||||||
@@ -50,6 +176,9 @@ impl SSHKey for DSAPrivKey<L1024N160,U192> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[test]
|
#[test]
|
||||||
fn read_dsa_examples() {
|
fn read_dsa_examples() {
|
||||||
@@ -57,7 +186,16 @@ fn read_dsa_examples() {
|
|||||||
|
|
||||||
for file in test_files.iter() {
|
for file in test_files.iter() {
|
||||||
let path = format!("testdata/ssh/{}",file);
|
let path = format!("testdata/ssh/{}",file);
|
||||||
let privkey = DSAPrivKey::<L1024N160,U192>::read_ssh_private_key_file(path);
|
let mkeypair = DSAKeyPair::<L1024N160,U1024,U192>::read_ssh_private_key_file(path);
|
||||||
assert!(privkey.is_ok());
|
match mkeypair {
|
||||||
|
Err(e) => assert!(false, format!("reading error: {:?}", e)),
|
||||||
|
Ok(keypair) => {
|
||||||
|
let buffer = [0,1,2,3,4,6,2];
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user