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

View File

@@ -20,6 +20,7 @@ pub trait ECCPoint : Sized {
}
}
#[derive(Debug,PartialEq)]
pub struct Point<T: EllipticCurve>
{
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::point::{ECCPoint,Point};
use hmac::{Hmac,Mac};
use std::fmt;
#[derive(PartialEq)]
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 {

View File

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

View File

@@ -1,5 +1,8 @@
use cryptonum::signed::*;
use cryptonum::unsigned::*;
use ecdsa::{ECDSAPair,ECDSAPublic,ECCPublicKey,ECDSAPrivate,ECCPrivateKey};
use ecdsa::curve::{P256,P384,P521};
use ecdsa::point::Point;
use std::io::{Read,Write};
use ssh::errors::{SSHKeyParseError,SSHKeyRenderError};
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
// one), but we need to infer what kind of key this is, and this appears
// to be the easiest / fastest way.
let mut ebuf = parse_openssh_buffer(inp)?;
let mut ibuf = parse_openssh_buffer(inp)?;
while ebuf[0] == 0 { ebuf.remove(0); }
while ibuf[0] == 0 { ibuf.remove(0); }
println!("ebuf: {:?}", ebuf);
println!("ibuf: {:?}", ibuf);
panic!("parse public info")
let curve = parse_openssh_string(inp)?;
match curve.as_ref() {
"nistp256" => {
let val = parse_openssh_buffer(inp)?;
if val[0] != 4 || val.len() != 65 {
return Err(SSHKeyParseError::InvalidECPointCompression);
}
let x = U256::from_bytes(&val[1..33]);
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>
@@ -39,40 +72,102 @@ impl SSHKey for ECDSAPair {
if check1 != check2 {
return Err(SSHKeyParseError::PrivateKeyCorruption);
}
let privkey_type = parse_openssh_string(inp)?;
if !Self::valid_keytype(&privkey_type) {
return Err(SSHKeyParseError::InconsistentKeyTypes("ssh-rsa".to_string(), privkey_type));
}
let comment = parse_openssh_string(inp)?;
let res = match ECDSAPair::parse_ssh_public_info(inp)? {
ECDSAPublic::P192(_) => return Err(SSHKeyParseError::PrivateKeyCorruption),
ECDSAPublic::P224(_) => return Err(SSHKeyParseError::PrivateKeyCorruption),
ECDSAPublic::P256(_) => {
let mut dbytes = parse_openssh_buffer(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() {
if ((idx+1) as u8) != byte? {
return Err(SSHKeyParseError::InvalidPadding);
}
}
panic!("parse private_info")
Ok((res, comment))
}
fn render_ssh_public_info<O: Write>(&self, out: &mut O) -> Result<(),SSHKeyRenderError>
{
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?
render_openssh_u32(out, 0xDEADBEEF)?; // ditto
render_openssh_string(out, "ssh-ecdsa")?;
panic!("render private info");
render_openssh_string(out, comment)?;
// add some padding (not quite sure why)
let mut i = comment.len();
while (i % 16) != 0 {
out.write(&[(i - comment.len() + 1) as u8])?;
i += 1;
self.render_ssh_public_info(out)?;
match self {
ECDSAPair::P192(_,_) =>
return Err(SSHKeyRenderError::IllegalECDSAKeyType("P192".to_string())),
ECDSAPair::P224(_,_) =>
return Err(SSHKeyRenderError::IllegalECDSAKeyType("P224".to_string())),
ECDSAPair::P256(_,pr) => { render_openssh_u32(out, 256/8)?; render_number(256, out, &pr.d)?; }
ECDSAPair::P384(_,pr) => { render_openssh_u32(out, 384/8)?; render_number(384, out, &pr.d)?; }
ECDSAPair::P521(_,pr) => { render_openssh_u32(out, 528/8)?; render_number(521, out, &pr.d)?; }
}
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,
InvalidPadding,
InvalidPublicKeyType,
BrokenPublicKeyLine
BrokenPublicKeyLine,
UnknownECDSACurve(String),
InvalidECPointCompression
}
impl From<DecodeError> for SSHKeyParseError {
@@ -39,7 +41,8 @@ impl From<io::Error> for SSHKeyParseError {
pub enum SSHKeyRenderError {
IOError(io::Error),
StringTooLong,
BufferTooLarge
BufferTooLarge,
IllegalECDSAKeyType(String)
}
impl From<io::Error> for SSHKeyRenderError {

View File

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