ecdsa private key

This commit is contained in:
nora 2024-08-26 18:14:21 +02:00
parent dcba4931e5
commit 1a093aa536
8 changed files with 147 additions and 28 deletions

View file

@ -81,8 +81,15 @@ impl<'a> Reader<'a> {
Ok(NameList(list))
}
pub fn mpint(&mut self) -> Result<MpInt<'a>> {
todo!("do correctly")
pub fn mpint(&mut self) -> Result<&'a [u8]> {
let mut s = self.string()?;
if s.first() == Some(&0) {
// Skip the leading zero byte in case the number is negative.
s = &s[1..];
}
Ok(s)
}
pub fn string(&mut self) -> Result<&'a [u8]> {
@ -152,6 +159,10 @@ impl Writer {
self.u8(v as u8);
}
pub fn current_length(&self) -> usize {
self.0.len()
}
pub fn finish(self) -> Vec<u8> {
self.0
}

View file

@ -23,7 +23,7 @@ impl AuthorizedKeys {
let key_blob = parts
.next()
.ok_or_else(|| Error("missing key on line".to_owned()))?;
let key_blob = base64::prelude::BASE64_STANDARD_NO_PAD
let key_blob = base64::prelude::BASE64_STANDARD
.decode(key_blob)
.map_err(|err| Error(format!("invalid base64 encoding for key: {err}")))?;
let comment = parts.next().unwrap_or_default();

View file

@ -55,6 +55,14 @@ impl Cipher {
}
}
}
pub(crate) fn block_size(&self) -> usize {
// this is the "minimum" block size in core SSH, so I assume it's here as well?
match self {
Self::None => 8,
Self::Aes256Ctr => 16, // looks like it takes the AES block size, even if AES-CTR isn't really a block cipher..
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]

View file

@ -179,7 +179,7 @@ impl EncryptedPrivateKeys {
let mut result_keys = Vec::new();
for pubkey in &self.public_keys {
let keytype = match pubkey {
let keytype = match *pubkey {
PublicKey::Ed25519 { public_key } => {
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#name-eddsa-keys>
let alg = p.utf8_string()?;
@ -208,7 +208,7 @@ impl EncryptedPrivateKeys {
}
let private_key = k.try_into().unwrap();
PrivateKey::Ed25519 {
public_key: *public_key,
public_key,
private_key,
}
}
@ -234,9 +234,16 @@ impl EncryptedPrivateKeys {
return Err(cluelessh_format::ParseError(format!("public key mismatch")));
}
let _d = p.mpint()?;
let d = p.mpint()?;
todo!()
let private_key = p256::ecdsa::SigningKey::from_slice(d).map_err(|_| {
cluelessh_format::ParseError(format!("invalid private key bytes"))
})?;
PrivateKey::EcdsaSha2NistP256 {
public_key,
private_key,
}
}
};
@ -339,10 +346,13 @@ impl PlaintextPrivateKey {
enc.string(self.comment.as_bytes());
// uh..., i don't really now how much i need to pad so YOLO this here
// TODO: pad properly.
enc.u8(1);
enc.u8(2);
let current_len = enc.current_length();
let block_size = params.cipher.block_size();
let pad_len = current_len.next_multiple_of(block_size) - current_len;
for i in 1..=(pad_len as u8) {
enc.u8(i);
}
let mut encrypted_private_keys = enc.finish();
@ -462,7 +472,7 @@ nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY
assert_eq!(decrypted.len(), 1);
let key = decrypted.first().unwrap();
assert_eq!(key.comment, "uwu");
assert!(matches!(key.private_key, PrivateKey::Ed25519 { .. }));
assert!(matches!(key.private_key, PrivateKey::EcdsaSha2NistP256 { .. }));
}
#[test]
@ -475,7 +485,22 @@ nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY
.unwrap();
let bytes = encrypted.to_bytes();
assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes);
std::fs::write(
"expected",
pem::parse(TEST_ECDSA_SHA2_NISTP256_NONE)
.unwrap()
.contents(),
)
.unwrap();
std::fs::write("found", &bytes).unwrap();
assert_eq!(
pem::parse(TEST_ECDSA_SHA2_NISTP256_NONE)
.unwrap()
.contents(),
bytes
);
}
#[test]

View file

@ -5,7 +5,6 @@
use std::fmt::Display;
use base64::Engine;
use ed25519_dalek::VerifyingKey;
use tracing::debug;
use cluelessh_format::{ParseError, Reader, Writer};
@ -29,17 +28,27 @@ impl PublicKey {
let k = match alg {
"ssh-ed25519" => {
// <https://datatracker.ietf.org/doc/html/rfc8709#name-public-key-format>
let len = p.u32()?;
if len != 32 {
return Err(ParseError(format!("incorrect ed25519 len: {len}")));
}
let public_key = p.array::<32>()?;
let public_key = VerifyingKey::from_bytes(&public_key)
let public_key = ed25519_dalek::VerifyingKey::from_bytes(&public_key)
.map_err(|_| ParseError(format!("invalid ed25519 public key")))?;
Self::Ed25519 { public_key }
}
"ecdsa-sha2-nistp256" => {
todo!()
// <https://datatracker.ietf.org/doc/html/rfc5656#section-3.1>
let params = p.utf8_string()?;
if params != "nistp256" {
return Err(ParseError(format!("curve parameter mismatch: {params}")));
}
let q = p.string()?;
let public_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(q)
.map_err(|_| ParseError("invalid public key format".to_owned()))?;
Self::EcdsaSha2NistP256 { public_key }
}
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
};
@ -51,6 +60,7 @@ impl PublicKey {
p.string(self.algorithm_name());
match self {
Self::Ed25519 { public_key } => {
// <https://datatracker.ietf.org/doc/html/rfc8709#name-public-key-format>
p.string(public_key.as_bytes());
}
Self::EcdsaSha2NistP256 { public_key } => {
@ -109,11 +119,47 @@ impl Display for PublicKey {
Self::EcdsaSha2NistP256 { .. } => {
let encoded_pubkey = b64encode(&self.to_wire_encoding());
write!(f, "{} {encoded_pubkey}", self.algorithm_name())
},
}
}
}
}
fn b64encode(bytes: &[u8]) -> String {
base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes)
base64::prelude::BASE64_STANDARD.encode(bytes)
}
#[cfg(test)]
mod tests {
use base64::Engine;
use super::PublicKey;
#[track_caller]
fn test_roundtrip(keys: &[&str]) {
for key_bytes in keys {
eprintln!("{key_bytes}");
let key_bytes: Vec<u8> = base64::prelude::BASE64_STANDARD.decode(key_bytes).unwrap();
let key = PublicKey::from_wire_encoding(&key_bytes).unwrap();
assert_eq!(key.to_wire_encoding(), key_bytes);
}
}
#[test]
fn ed25519() {
test_roundtrip(&[
"AAAAC3NzaC1lZDI1NTE5AAAAIJJKT1n+xPwS4ECXXPVB5U5gWwMpqa+FMvVuyFwbfvEg",
"AAAAC3NzaC1lZDI1NTE5AAAAINZ1yLdDhI2Vou/9qrPIUP8RU8Sg0WxLI2njtP5hkdL7",
"AAAAC3NzaC1lZDI1NTE5AAAAIAIIWlDvWkMEX8XIu6lxvd4cFOxeFUpH4ZReKuyS3h9l",
]);
}
#[test]
fn ecdsa_sha2_nistp256() {
test_roundtrip(&[
"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHZTdlJoLNb701EWnahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY8=",
"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCv8bAwK5tZBEpOgFe6tmnog6GHKzeXnOK/qewbH4yiGb9fq4LkSY8oK3WhVZdIwtc1n8j9dNc4aGMURNlVBNKc=",
]);
}
}