Skip to content

Commit

Permalink
Handle verification of keys using elliptic curve (#132)
Browse files Browse the repository at this point in the history
* Handle verification of keys using elliptic curve

This commit introduces support for handling certificates using elliptic
curve public keys.

The implementation relies on the `ring` crate, which currently is the
only one capable of supporting both ECDSA p256 and ECDSA p384.

Signed-off-by: Flavio Castelli <[email protected]>

* picky: fix ssh module compilation

Co-authored-by: Benoît CORTIER <[email protected]>
  • Loading branch information
flavio and CBenoit authored Mar 4, 2022
1 parent 3bbd722 commit bbc4fb5
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 31 deletions.
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions picky/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ aes-gcm = { version = "0.9.4", optional = true }
bcrypt-pbkdf = { version = "0.6", optional = true }
block-modes = { version = "0.8", optional = true }
aes = { version = "0.7.5", features = ["ctr"], default-features = false, optional = true }
ring = "0.16.20"

[dev-dependencies]
pretty_assertions = "1.0.0"
hex = "0.4.3"
cfg-if = "1.0.0"
ring = "0.16.20"
rstest = "0.12.0"

[features]
default = ["x509", "jose", "http_signature", "http_trait_impl"]
Expand Down
8 changes: 8 additions & 0 deletions picky/src/http/http_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,9 @@ const HTTP_SIG_ALGO_RSA_SHA2_512: &str = "rsa-sha2-512";
const HTTP_SIG_ALGO_RSA_SHA3_384: &str = "rsa-sha3-384";
const HTTP_SIG_ALGO_RSA_SHA3_512: &str = "rsa-sha3-512";

const HTTP_SIG_ALGO_ECDSA_SHA_256: &str = "ecdsa-sha256";
const HTTP_SIG_ALGO_ECDSA_SHA_384: &str = "ecdsa-sha384";

fn to_http_sig_algo_str(algo: SignatureAlgorithm) -> &'static str {
match algo {
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => HTTP_SIG_ALGO_RSA_MD5,
Expand All @@ -839,6 +842,9 @@ fn to_http_sig_algo_str(algo: SignatureAlgorithm) -> &'static str {
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => HTTP_SIG_ALGO_RSA_SHA_512,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => HTTP_SIG_ALGO_RSA_SHA3_384,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => HTTP_SIG_ALGO_RSA_SHA3_512,
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_ECDSA_SHA_256,
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_ECDSA_SHA_384,
SignatureAlgorithm::Ecdsa(_) => "ECDSA unsupported algorithm",
}
}

Expand All @@ -860,6 +866,8 @@ fn from_http_sig_algo_str(s: &str) -> Option<SignatureAlgorithm> {
}
HTTP_SIG_ALGO_RSA_SHA3_384 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384)),
HTTP_SIG_ALGO_RSA_SHA3_512 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512)),
HTTP_SIG_ALGO_ECDSA_SHA_256 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)),
HTTP_SIG_ALGO_ECDSA_SHA_384 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)),
_ => None,
}
}
Expand Down
162 changes: 145 additions & 17 deletions picky/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

use crate::pem::{parse_pem, Pem, PemError};
use num_bigint_dig::traits::ModInverse;
use picky_asn1::wrapper::{BitStringAsn1Container, IntegerAsn1, OctetStringAsn1Container};
use picky_asn1::wrapper::{BitStringAsn1, BitStringAsn1Container, IntegerAsn1, OctetStringAsn1Container};
use picky_asn1_der::Asn1DerError;
use picky_asn1_x509::{private_key_info, PrivateKeyInfo, PrivateKeyValue, SubjectPublicKeyInfo};
use picky_asn1_x509::{oids, private_key_info, PrivateKeyInfo, PrivateKeyValue, SubjectPublicKeyInfo};
use rsa::{BigUint, RsaPrivateKey, RsaPublicKey};
use thiserror::Error;

Expand Down Expand Up @@ -127,6 +127,70 @@ impl TryFrom<&'_ PrivateKey> for RsaPublicKey {
}
}

#[derive(Debug)]
pub(crate) struct EcdsaKeypair<'a> {
pub(crate) private_key: Vec<u8>,
pub(crate) public_key: &'a [u8],
pub(crate) curve: EcdsaCurve,
}

#[derive(Debug, PartialEq)]
pub(crate) enum EcdsaCurve {
Nist256,
Nist384,
Nist512,
}

impl<'a> TryFrom<&'a PrivateKey> for EcdsaKeypair<'a> {
type Error = KeyError;

fn try_from(v: &'a PrivateKey) -> Result<Self, Self::Error> {
match &v.as_inner().private_key {
private_key_info::PrivateKeyValue::RSA(_) => Err(KeyError::EC {
context: "EC keypair cannot be built from RSA private key".to_string(),
}),
private_key_info::PrivateKeyValue::EC(OctetStringAsn1Container(private_key)) => {
let private_key_data = private_key.private_key.to_vec();
let public_key_data = private_key.public_key.payload_view();

let curve = match &private_key.parameters.0 .0 {
Some(ec_parameters) => match ec_parameters {
picky_asn1_x509::EcParameters::NamedCurve(curve) => {
let oid = Into::<String>::into(&curve.0);
match oid.as_str() {
oids::SECP256R1 => Ok(EcdsaCurve::Nist256),
oids::SECP384R1 => Ok(EcdsaCurve::Nist384),
oids::SECP521R1 => Ok(EcdsaCurve::Nist512),
unknown => Err(KeyError::EC {
context: format!("Unknown curve type: {}", unknown),
}),
}
}
},
None => Err(KeyError::EC {
context: "EC keypair cannot be built when curve type is not provided by private key"
.to_string(),
}),
}?;

if public_key_data.is_empty() {
Err(KeyError::EC {
context:
"EC keypair cannot be built from EC private key that doesn't have a bundled private key"
.to_string(),
})
} else {
Ok(EcdsaKeypair {
private_key: private_key_data,
public_key: public_key_data,
curve,
})
}
}
}
}
}

impl PrivateKey {
pub fn from_rsa_components(
modulus: &BigUint,
Expand Down Expand Up @@ -356,6 +420,31 @@ impl TryFrom<&'_ PublicKey> for RsaPublicKey {
}
}

pub(crate) struct EcdsaPublicKey<'a> {
pub(crate) data: &'a [u8],
}

impl<'a> TryFrom<&'a PublicKey> for EcdsaPublicKey<'a> {
type Error = KeyError;

fn try_from(v: &'a PublicKey) -> Result<Self, Self::Error> {
use picky_asn1_x509::PublicKey as InnerPublicKey;

match &v.as_inner().subject_public_key {
InnerPublicKey::Rsa(_) => Err(KeyError::EC {
context: "EC public key cannot be constructed from RSA public key".to_string(),
}),
InnerPublicKey::Ec(BitStringAsn1(bitstring)) => {
let data = bitstring.payload_view();
Ok(EcdsaPublicKey { data })
}
InnerPublicKey::Ed(_) => Err(KeyError::EC {
context: "EC public key cannot be constructed from ED25519 public key".to_string(),
}),
}
}
}

impl PublicKey {
pub fn from_rsa_components(modulus: &BigUint, public_exponent: &BigUint) -> Self {
PublicKey(SubjectPublicKeyInfo::new_rsa_key(
Expand Down Expand Up @@ -430,6 +519,7 @@ mod tests {
use crate::hash::HashAlgorithm;
use crate::signature::SignatureAlgorithm;
use rsa::PublicKeyParts;
use rstest::*;

cfg_if::cfg_if! { if #[cfg(feature = "x509")] {
use crate::x509::{certificate::CertificateBuilder, date::UtcDate, name::DirectoryName};
Expand Down Expand Up @@ -581,14 +671,6 @@ mod tests {
ring::signature::RsaKeyPair::from_pkcs8(&pkcs8).unwrap();
}

const EC_PRIVATE_KEY_PEM: &str = "-----BEGIN EC PRIVATE KEY-----\n\
MIHcAgEBBEIBhqphIGu2PmlcEb6xADhhSCpgPUulB0s4L2qOgolRgaBx4fNgINFE\n\
mBsSyHJncsWG8WFEuUzAYy/YKz2lP0Qx6Z2gBwYFK4EEACOhgYkDgYYABABwBevJ\n\
w/+Xh6I98ruzoTX3MNTsbgnc+glenJRCbEJkjbJrObFhbfgqP52r1lAy2RxuShGi\n\
NYJJzNPT6vR1abS32QFtvTH7YbYa6OWk9dtGNY/cYxgx1nQyhUuofdW7qbbfu/Ww\n\
TP2oFsPXRAavZCh4AbWUn8bAHmzNRyuJonQBKlQlVQ==\n\
-----END EC PRIVATE KEY-----";

const PKCS8_EC_PRIVATE_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKZqrmOg/cDZ4tPCn\n\
4LROs145nxx+ssufvflL8cROxFmhRANCAARmU90fCSTsncefY7hVeKw1WIg/YQmT\n\
Expand All @@ -600,18 +682,64 @@ mod tests {
bJM105PImXUuqTMyqSmX96/m7zFfyh/DQQbyXIo3E07qifCPMw9/oQ==\n\
-----END PUBLIC KEY-----";

#[test]
fn private_key_from_ec_pem() {
PrivateKey::from_pem_str(EC_PRIVATE_KEY_PEM).unwrap();
const EC_PRIVATE_KEY_NIST256_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICHio5XUa+RbeFfGtGHfbPWehTFJJtCB4/izKHJ9Vm+goAoGCCqGSM49
AwEHoUQDQgAEh7ZqcI6f0tgqq7nqdcxWM6P4GGCfkWc4q11uXFjtXOKHKCV3LzMY
g8/V1PD/YOh0HodRJAjkjXub8AmYxiTcXw==
-----END EC PRIVATE KEY-----"#;

const EC_PRIVATE_KEY_NIST384_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDDT8VOfdzHbIRaWOO1F0vgotY2qM2FfYS3zpdKE7Vqbh26hFsUw+iaG
GmGnT+29kg+gBwYFK4EEACKhZANiAAQFvVVUKRdN3/bqaEpDA1aHu8FEd3ujuyS0
AadG6QAiZxH37BGumBcyTTeGHyArqb+GTpsHTUXASbP+P+p5JgkfF9wBMF1SVTvu
ACZOYcqzGbsAXXdMYqewckhc42ye0u0=
-----END EC PRIVATE KEY-----"#;

const EC_PRIVATE_KEY_NIST512_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIBhqphIGu2PmlcEb6xADhhSCpgPUulB0s4L2qOgolRgaBx4fNgINFE
mBsSyHJncsWG8WFEuUzAYy/YKz2lP0Qx6Z2gBwYFK4EEACOhgYkDgYYABABwBevJ
w/+Xh6I98ruzoTX3MNTsbgnc+glenJRCbEJkjbJrObFhbfgqP52r1lAy2RxuShGi
NYJJzNPT6vR1abS32QFtvTH7YbYa6OWk9dtGNY/cYxgx1nQyhUuofdW7qbbfu/Ww
TP2oFsPXRAavZCh4AbWUn8bAHmzNRyuJonQBKlQlVQ==
-----END EC PRIVATE KEY-----"#;

#[rstest]
#[case(EC_PRIVATE_KEY_NIST256_PEM)]
#[case(EC_PRIVATE_KEY_NIST384_PEM)]
#[case(EC_PRIVATE_KEY_NIST512_PEM)]
#[case(PKCS8_EC_PRIVATE_KEY_PEM)]
fn private_key_from_ec_pem(#[case] key_pem: &str) {
PrivateKey::from_pem_str(key_pem).unwrap();
}

#[test]
fn private_key_from_pkcs8_ec_pem() {
PrivateKey::from_pem_str(PKCS8_EC_PRIVATE_KEY_PEM).unwrap();
fn public_key_from_ec_pem() {
PublicKey::from_pem_str(EC_PUBLIC_KEY_PEM).unwrap();
}

#[test]
fn public_key_from_ec_pem() {
PublicKey::from_pem_str(EC_PUBLIC_KEY_PEM).unwrap();
fn ecdsa_public_key_conversions() {
// ECDSA public key conversion works
let pk: &PublicKey = &PublicKey::from_pem_str(EC_PUBLIC_KEY_PEM).unwrap();
let epk: Result<EcdsaPublicKey, KeyError> = pk.try_into();
assert!(epk.is_ok());

// PEM public key conversion fails with an error
let pk: &PublicKey = &PublicKey::from_pem_str(RSA_PUBLIC_KEY_PEM).unwrap();
let epk: Result<EcdsaPublicKey, KeyError> = pk.try_into();
assert!(epk.is_err());
assert!(matches!(epk, Err(KeyError::EC { context: _ })));

// TODO: add check for attempted conversion from ED keys - which are not supported yet
}

#[rstest]
#[case(EC_PRIVATE_KEY_NIST256_PEM, EcdsaCurve::Nist256)]
#[case(EC_PRIVATE_KEY_NIST384_PEM, EcdsaCurve::Nist384)]
#[case(EC_PRIVATE_KEY_NIST512_PEM, EcdsaCurve::Nist512)]
fn ecdsa_key_pair_from_ec_private_key(#[case] key: &str, #[case] curve: EcdsaCurve) {
let pk = PrivateKey::from_pem_str(key).unwrap();
let pair = EcdsaKeypair::try_from(&pk).unwrap();
assert_eq!(curve, pair.curve);
}
}
Loading

0 comments on commit bbc4fb5

Please sign in to comment.