ECDSA SSH key support.

This commit is contained in:
2019-04-17 21:20:52 -05:00
parent 1eba2d1709
commit ad484877cf
7 changed files with 219 additions and 34 deletions

View File

@@ -1,11 +1,12 @@
use cryptonum::signed::{I192,I256,I384,I576}; use cryptonum::signed::{I192,I256,I384,I576};
use cryptonum::unsigned::{Decoder}; use cryptonum::unsigned::{Decoder};
use cryptonum::unsigned::{U192,U256,U384,U576}; use cryptonum::unsigned::{U192,U256,U384,U576};
use std::fmt::Debug;
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub trait EllipticCurve { pub trait EllipticCurve {
type Unsigned : Clone; type Unsigned : Clone;
type Signed : Clone; type Signed : Clone + Debug + PartialEq;
fn size() -> usize; fn size() -> usize;
fn p() -> Self::Unsigned; fn p() -> Self::Unsigned;
@@ -18,6 +19,7 @@ pub trait EllipticCurve {
fn Gy() -> Self::Signed; fn Gy() -> Self::Signed;
} }
#[derive(Debug,PartialEq)]
pub enum P192 {} pub enum P192 {}
impl EllipticCurve for P192 { impl EllipticCurve for P192 {
@@ -61,6 +63,7 @@ impl EllipticCurve for P192 {
} }
} }
#[derive(Debug,PartialEq)]
pub enum P224 {} pub enum P224 {}
impl EllipticCurve for P224 { impl EllipticCurve for P224 {
@@ -143,6 +146,7 @@ impl EllipticCurve for P224 {
} }
} }
#[derive(Debug,PartialEq)]
pub enum P256 {} pub enum P256 {}
impl EllipticCurve for P256 { impl EllipticCurve for P256 {
@@ -226,6 +230,7 @@ impl EllipticCurve for P256 {
} }
} }
#[derive(Debug,PartialEq)]
pub enum P384 {} pub enum P384 {}
impl EllipticCurve for P384 { impl EllipticCurve for P384 {
@@ -322,6 +327,7 @@ impl EllipticCurve for P384 {
} }
} }
#[derive(Debug,PartialEq)]
pub enum P521 {} pub enum P521 {}
impl EllipticCurve for P521 { impl EllipticCurve for P521 {

View File

@@ -20,6 +20,7 @@ pub trait ECCPoint : Sized {
} }
} }
#[derive(Debug,PartialEq)]
pub struct Point<T: EllipticCurve> pub struct Point<T: EllipticCurve>
{ {
pub x: T::Signed, pub x: T::Signed,

View File

@@ -5,9 +5,18 @@ use dsa::rfc6979::{DSASignature,KIterator,bits2int};
use ecdsa::curve::{EllipticCurve,P192,P224,P256,P384,P521}; use ecdsa::curve::{EllipticCurve,P192,P224,P256,P384,P521};
use ecdsa::point::{ECCPoint,Point}; use ecdsa::point::{ECCPoint,Point};
use hmac::{Hmac,Mac}; use hmac::{Hmac,Mac};
use std::fmt;
#[derive(PartialEq)]
pub struct ECCPrivateKey<Curve: EllipticCurve> { pub struct ECCPrivateKey<Curve: EllipticCurve> {
d: Curve::Unsigned pub(crate) d: Curve::Unsigned
}
impl<Curve: EllipticCurve> fmt::Debug for ECCPrivateKey<Curve> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(),fmt::Error>
{
f.write_str("<ECCPrivateKey>")
}
} }
pub enum ECDSAPrivate { pub enum ECDSAPrivate {

View File

@@ -8,8 +8,9 @@ use hmac::{Hmac,Mac};
use simple_asn1::{ASN1Block,ASN1Class,ASN1DecodeErr,ASN1EncodeErr,FromASN1,ToASN1}; use simple_asn1::{ASN1Block,ASN1Class,ASN1DecodeErr,ASN1EncodeErr,FromASN1,ToASN1};
use std::cmp::min; use std::cmp::min;
#[derive(Debug,PartialEq)]
pub struct ECCPublicKey<Curve: EllipticCurve> { pub struct ECCPublicKey<Curve: EllipticCurve> {
q: Point<Curve> pub(crate) q: Point<Curve>
} }
pub enum ECDSAPublic { pub enum ECDSAPublic {

View File

@@ -1,5 +1,8 @@
use cryptonum::signed::*;
use cryptonum::unsigned::*; use cryptonum::unsigned::*;
use ecdsa::{ECDSAPair,ECDSAPublic,ECCPublicKey,ECDSAPrivate,ECCPrivateKey}; use ecdsa::{ECDSAPair,ECDSAPublic,ECCPublicKey,ECDSAPrivate,ECCPrivateKey};
use ecdsa::curve::{P256,P384,P521};
use ecdsa::point::Point;
use std::io::{Read,Write}; use std::io::{Read,Write};
use ssh::errors::{SSHKeyParseError,SSHKeyRenderError}; use ssh::errors::{SSHKeyParseError,SSHKeyRenderError};
use ssh::frame::*; use ssh::frame::*;
@@ -21,15 +24,45 @@ impl SSHKey for ECDSAPair {
// that we didn't know the number format was the same as the buffer // that we didn't know the number format was the same as the buffer
// one), but we need to infer what kind of key this is, and this appears // one), but we need to infer what kind of key this is, and this appears
// to be the easiest / fastest way. // to be the easiest / fastest way.
let mut ebuf = parse_openssh_buffer(inp)?; let curve = parse_openssh_string(inp)?;
let mut ibuf = parse_openssh_buffer(inp)?; match curve.as_ref() {
"nistp256" => {
while ebuf[0] == 0 { ebuf.remove(0); } let val = parse_openssh_buffer(inp)?;
while ibuf[0] == 0 { ibuf.remove(0); } if val[0] != 4 || val.len() != 65 {
return Err(SSHKeyParseError::InvalidECPointCompression);
println!("ebuf: {:?}", ebuf); }
println!("ibuf: {:?}", ibuf); let x = U256::from_bytes(&val[1..33]);
panic!("parse public info") let y = U256::from_bytes(&val[33..]);
let p = Point::<P256>{ x: I256::from(x), y: I256::from(y) };
let pbl = ECCPublicKey::<P256>::new(p);
Ok(ECDSAPublic::P256(pbl))
}
"nistp384" => {
let val = parse_openssh_buffer(inp)?;
if val[0] != 4 || val.len() != 97 {
return Err(SSHKeyParseError::InvalidECPointCompression);
}
let x = U384::from_bytes(&val[1..49]);
let y = U384::from_bytes(&val[49..]);
let p = Point::<P384>{ x: I384::from(x), y: I384::from(y) };
let pbl = ECCPublicKey::<P384>::new(p);
Ok(ECDSAPublic::P384(pbl))
}
"nistp521" => {
let val = parse_openssh_buffer(inp)?;
if val[0] != 4 || val.len() != 133 {
return Err(SSHKeyParseError::InvalidECPointCompression);
}
let x = U576::from_bytes(&val[1..67]);
let y = U576::from_bytes(&val[67..]);
let p = Point::<P521>{ x: I576::from(x), y: I576::from(y) };
let pbl = ECCPublicKey::<P521>::new(p);
Ok(ECDSAPublic::P521(pbl))
}
_ => {
return Err(SSHKeyParseError::UnknownECDSACurve(curve))
}
}
} }
fn parse_ssh_private_info<I: Read>(inp: &mut I) -> Result<(Self::Private,String),SSHKeyParseError> fn parse_ssh_private_info<I: Read>(inp: &mut I) -> Result<(Self::Private,String),SSHKeyParseError>
@@ -39,40 +72,102 @@ impl SSHKey for ECDSAPair {
if check1 != check2 { if check1 != check2 {
return Err(SSHKeyParseError::PrivateKeyCorruption); return Err(SSHKeyParseError::PrivateKeyCorruption);
} }
let privkey_type = parse_openssh_string(inp)?; let res = match ECDSAPair::parse_ssh_public_info(inp)? {
if !Self::valid_keytype(&privkey_type) { ECDSAPublic::P192(_) => return Err(SSHKeyParseError::PrivateKeyCorruption),
return Err(SSHKeyParseError::InconsistentKeyTypes("ssh-rsa".to_string(), privkey_type)); ECDSAPublic::P224(_) => return Err(SSHKeyParseError::PrivateKeyCorruption),
} ECDSAPublic::P256(_) => {
let mut dbytes = parse_openssh_buffer(inp)?;
let comment = parse_openssh_string(inp)?; while dbytes[0] == 0 { dbytes.remove(0); }
assert!(dbytes.len() <= 32);
let d = U256::from_bytes(&dbytes);
ECDSAPrivate::P256(ECCPrivateKey::<P256>::new(d))
}
ECDSAPublic::P384(_) => {
let mut dbytes = parse_openssh_buffer(inp)?;
while dbytes[0] == 0 { dbytes.remove(0); }
assert!(dbytes.len() <= 48);
let d = U384::from_bytes(&dbytes);
ECDSAPrivate::P384(ECCPrivateKey::<P384>::new(d))
}
ECDSAPublic::P521(_) => {
let mut dbytes = parse_openssh_buffer(inp)?;
while dbytes[0] == 0 { dbytes.remove(0); }
assert!(dbytes.len() <= 66);
let d = U576::from_bytes(&dbytes);
ECDSAPrivate::P521(ECCPrivateKey::<P521>::new(d))
}
};
let comment = parse_openssh_string(inp)?;
for (idx,byte) in inp.bytes().enumerate() { for (idx,byte) in inp.bytes().enumerate() {
if ((idx+1) as u8) != byte? { if ((idx+1) as u8) != byte? {
return Err(SSHKeyParseError::InvalidPadding); return Err(SSHKeyParseError::InvalidPadding);
} }
} }
panic!("parse private_info") Ok((res, comment))
} }
fn render_ssh_public_info<O: Write>(&self, out: &mut O) -> Result<(),SSHKeyRenderError> fn render_ssh_public_info<O: Write>(&self, out: &mut O) -> Result<(),SSHKeyRenderError>
{ {
render_openssh_string(out, "ssh-ecdsa")?; render_openssh_string(out, "ssh-ecdsa")?;
panic!("render public info") match self {
ECDSAPair::P192(_,_) =>
return Err(SSHKeyRenderError::IllegalECDSAKeyType("P192".to_string())),
ECDSAPair::P224(_,_) =>
return Err(SSHKeyRenderError::IllegalECDSAKeyType("P224".to_string())),
ECDSAPair::P256(pu,_) => {
render_openssh_string(out, "nistp256")?;
let mut vec = Vec::with_capacity(66);
vec.write(&[4u8])?;
render_number(256, &mut vec, &U256::from(pu.q.x.clone()))?;
render_number(256, &mut vec, &U256::from(pu.q.y.clone()))?;
render_openssh_buffer(out, &vec)?;
}
ECDSAPair::P384(pu,_) => {
render_openssh_string(out, "nistp384")?;
let mut vec = Vec::with_capacity(66);
vec.write(&[4u8])?;
render_number(384, &mut vec, &U384::from(pu.q.x.clone()))?;
render_number(384, &mut vec, &U384::from(pu.q.y.clone()))?;
render_openssh_buffer(out, &vec)?;
}
ECDSAPair::P521(pu,_) => {
render_openssh_string(out, "nistp521")?;
let mut vec = Vec::with_capacity(66);
vec.write(&[4u8])?;
render_number(521, &mut vec, &U576::from(pu.q.x.clone()))?;
render_number(521, &mut vec, &U576::from(pu.q.y.clone()))?;
render_openssh_buffer(out, &vec)?;
}
}
Ok(())
} }
fn render_ssh_private_info<O: Write>(&self, out: &mut O, comment: &str) -> Result<(),SSHKeyRenderError> fn render_ssh_private_info<O: Write>(&self, out: &mut O) -> Result<(),SSHKeyRenderError>
{ {
render_openssh_u32(out, 0xDEADBEEF)?; // FIXME: Any reason for this to be random? self.render_ssh_public_info(out)?;
render_openssh_u32(out, 0xDEADBEEF)?; // ditto match self {
render_openssh_string(out, "ssh-ecdsa")?; ECDSAPair::P192(_,_) =>
panic!("render private info"); return Err(SSHKeyRenderError::IllegalECDSAKeyType("P192".to_string())),
render_openssh_string(out, comment)?; ECDSAPair::P224(_,_) =>
// add some padding (not quite sure why) return Err(SSHKeyRenderError::IllegalECDSAKeyType("P224".to_string())),
let mut i = comment.len(); ECDSAPair::P256(_,pr) => { render_openssh_u32(out, 256/8)?; render_number(256, out, &pr.d)?; }
while (i % 16) != 0 { ECDSAPair::P384(_,pr) => { render_openssh_u32(out, 384/8)?; render_number(384, out, &pr.d)?; }
out.write(&[(i - comment.len() + 1) as u8])?; ECDSAPair::P521(_,pr) => { render_openssh_u32(out, 528/8)?; render_number(521, out, &pr.d)?; }
i += 1;
} }
Ok(()) Ok(())
} }
} }
fn render_number<O,N>(bitlen: usize, out: &mut O, val: &N) -> Result<(),SSHKeyRenderError>
where
O: Write,
N: Encoder
{
let mut outvec = Vec::new();
outvec.write(&val.to_bytes())?;
while outvec.len() < ((bitlen + 7) / 8) { outvec.insert(0,0); }
while outvec.len() > ((bitlen + 7) / 8) { outvec.remove(0); }
out.write(&outvec)?;
Ok(())
}

View File

@@ -20,7 +20,9 @@ pub enum SSHKeyParseError
InvalidPrivateKeyValue, InvalidPrivateKeyValue,
InvalidPadding, InvalidPadding,
InvalidPublicKeyType, InvalidPublicKeyType,
BrokenPublicKeyLine BrokenPublicKeyLine,
UnknownECDSACurve(String),
InvalidECPointCompression
} }
impl From<DecodeError> for SSHKeyParseError { impl From<DecodeError> for SSHKeyParseError {
@@ -39,7 +41,8 @@ impl From<io::Error> for SSHKeyParseError {
pub enum SSHKeyRenderError { pub enum SSHKeyRenderError {
IOError(io::Error), IOError(io::Error),
StringTooLong, StringTooLong,
BufferTooLarge BufferTooLarge,
IllegalECDSAKeyType(String)
} }
impl From<io::Error> for SSHKeyRenderError { impl From<io::Error> for SSHKeyRenderError {

View File

@@ -1,4 +1,5 @@
mod dsa; mod dsa;
mod ecdsa;
mod errors; mod errors;
mod frame; mod frame;
mod rsa; mod rsa;
@@ -154,6 +155,8 @@ pub fn write_ssh_keyfile<KP,P>(path: P, x: &KP, comment: &str) -> Result<(),SSHK
#[cfg(test)] #[cfg(test)]
use dsa::{DSAKeyPair,DSAPublicKey,L1024N160}; use dsa::{DSAKeyPair,DSAPublicKey,L1024N160};
#[cfg(test)] #[cfg(test)]
use ecdsa::ECDSAPair;
#[cfg(test)]
use rsa::{RSAPair,RSAPublic,SIGNING_HASH_SHA256}; use rsa::{RSAPair,RSAPublic,SIGNING_HASH_SHA256};
#[cfg(test)] #[cfg(test)]
use sha2::Sha256; use sha2::Sha256;
@@ -259,3 +262,70 @@ fn rsa_examples() {
} }
} }
} }
#[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")
}
}
}
}
}
}
}