From d3e565ef55e3eef9553395ce312879c742696034 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 Dec 2019 15:35:00 +0000 Subject: [PATCH 1/5] Derive PartialEq for SlotId --- src/key.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/key.rs b/src/key.rs index 96392c7..b54fa53 100644 --- a/src/key.rs +++ b/src/key.rs @@ -52,7 +52,7 @@ use std::convert::TryFrom; /// Slot identifiers. /// -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum SlotId { /// This certificate and its associated private key is used to authenticate the card /// and the cardholder. This slot is used for things like system login. The end user @@ -136,7 +136,7 @@ impl SlotId { /// Retired slot IDs. #[allow(missing_docs)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum RetiredSlotId { R1, R2, From 9ee1494c6ff539cf338da46801825b8a1f9ff2b6 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 Dec 2019 15:54:12 +0000 Subject: [PATCH 2/5] Parse RSA public keys within certificates --- Cargo.toml | 4 ++ src/certificate.rs | 141 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c073861..796cdb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,15 +19,19 @@ keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"] maintenance = { status = "experimental" } [dependencies] +der-parser = "3" des = "0.3" getrandom = "0.1" hmac = "0.7" log = "0.4" +nom = "5" pbkdf2 = "0.3" pcsc = "2" +rsa = "0.1.4" secrecy = "0.5" sha-1 = "0.8" subtle = "2" +x509-parser = "0.6" zeroize = "1" [dev-dependencies] diff --git a/src/certificate.rs b/src/certificate.rs index 6dbf4a5..feeaf2c 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -31,15 +31,67 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{ - consts::*, error::Error, key::SlotId, serialization::*, transaction::Transaction, - yubikey::YubiKey, Buffer, + consts::*, + error::Error, + key::{AlgorithmId, SlotId}, + serialization::*, + transaction::Transaction, + yubikey::YubiKey, + Buffer, }; use log::error; +use rsa::{PublicKey, RSAPublicKey}; +use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo}; use zeroize::Zeroizing; +/// Information about a public key within a [`Certificate`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PublicKeyInfo { + /// RSA keys + Rsa { + /// RSA algorithm + algorithm: AlgorithmId, + + /// Public key + pubkey: RSAPublicKey, + }, +} + +impl PublicKeyInfo { + fn parse(subject_pki: &SubjectPublicKeyInfo<'_>) -> Result { + match subject_pki.algorithm.algorithm.to_string().as_str() { + // RSA encryption + "1.2.840.113549.1.1.1" => { + let pubkey = read_pki::rsa_pubkey(subject_pki.subject_public_key.data)?; + + Ok(PublicKeyInfo::Rsa { + algorithm: match pubkey.n().bits() { + 1024 => AlgorithmId::Rsa1024, + 2048 => AlgorithmId::Rsa2048, + _ => return Err(Error::AlgorithmError), + }, + pubkey, + }) + } + _ => Err(Error::InvalidObject), + } + } + + /// Returns the algorithm that this public key can be used with. + pub fn algorithm(&self) -> AlgorithmId { + match self { + PublicKeyInfo::Rsa { algorithm, .. } => *algorithm, + } + } +} + /// Certificates #[derive(Clone, Debug)] -pub struct Certificate(Buffer); +pub struct Certificate { + subject: String, + subject_pki: PublicKeyInfo, + data: Buffer, +} impl Certificate { /// Read a certificate from the given slot in the YubiKey @@ -51,14 +103,14 @@ impl Certificate { return Err(Error::InvalidObject); } - Ok(Certificate(buf)) + Certificate::new(buf) } /// Write this certificate into the YubiKey in the given slot pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: u8) -> Result<(), Error> { let max_size = yubikey.obj_size_max(); let txn = yubikey.begin_transaction()?; - write_certificate(&txn, slot, Some(&self.0), certinfo, max_size) + write_certificate(&txn, slot, Some(&self.data), certinfo, max_size) } /// Delete a certificate located at the given slot of the given YubiKey @@ -77,18 +129,40 @@ impl Certificate { return Err(Error::SizeError); } - Ok(Certificate(cert)) + let parsed_cert = match parse_x509_der(&cert) { + Ok((_, cert)) => cert, + _ => return Err(Error::InvalidObject), + }; + + let subject = format!("{}", parsed_cert.tbs_certificate.subject); + let subject_pki = PublicKeyInfo::parse(&parsed_cert.tbs_certificate.subject_pki)?; + + Ok(Certificate { + subject, + subject_pki, + data: cert, + }) + } + + /// Returns the SubjectName field of the certificate. + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the SubjectPublicKeyInfo field of the certificate. + pub fn subject_pki(&self) -> &PublicKeyInfo { + &self.subject_pki } /// Extract the inner buffer pub fn into_buffer(self) -> Buffer { - self.0 + self.data } } impl AsRef<[u8]> for Certificate { fn as_ref(&self) -> &[u8] { - self.0.as_ref() + self.data.as_ref() } } @@ -175,3 +249,54 @@ pub(crate) fn write_certificate( txn.save_object(object_id, &buf[..offset]) } + +mod read_pki { + use der_parser::{ + ber::BerObjectContent, + der::{parse_der_integer, DerObject}, + error::BerError, + *, + }; + use nom::{combinator, IResult}; + use rsa::{BigUint, RSAPublicKey}; + + use crate::error::Error; + + /// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1): + /// ```text + /// RSAPublicKey ::= SEQUENCE { + /// modulus INTEGER, -- n + /// publicExponent INTEGER -- e + /// } + /// ``` + pub(super) fn rsa_pubkey(encoded: &[u8]) -> Result { + fn parse_rsa_pubkey(i: &[u8]) -> IResult<&[u8], DerObject<'_>, BerError> { + parse_der_sequence_defined!(i, parse_der_integer >> parse_der_integer) + } + + fn rsa_pubkey_parts(i: &[u8]) -> IResult<&[u8], (BigUint, BigUint), BerError> { + combinator::map(parse_rsa_pubkey, |object| { + let seq = object.as_sequence().expect("is DER sequence"); + assert_eq!(seq.len(), 2); + + let n = match seq[0].content { + BerObjectContent::Integer(s) => BigUint::from_bytes_be(s), + _ => panic!("expected DER integer"), + }; + let e = match seq[1].content { + BerObjectContent::Integer(s) => BigUint::from_bytes_be(s), + _ => panic!("expected DER integer"), + }; + + (n, e) + })(i) + } + + let (n, e) = match rsa_pubkey_parts(encoded) { + Ok((_, res)) => res, + _ => return Err(Error::InvalidObject), + }; + + RSAPublicKey::new(n, e).map_err(|_| Error::InvalidObject) + } +} From e72ee5c60eb34fd1faf224a04b3eacba61099a01 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 Dec 2019 16:54:22 +0000 Subject: [PATCH 3/5] Parse EC public keys within certificates --- src/certificate.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/certificate.rs b/src/certificate.rs index feeaf2c..7131fb4 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -44,6 +44,28 @@ use rsa::{PublicKey, RSAPublicKey}; use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo}; use zeroize::Zeroizing; +/// An encoded point for some elliptic curve. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EcPoint { + /// A point in compressed form. + Compressed { + /// EC algorithm + algorithm: AlgorithmId, + + /// Encoded point + bytes: Buffer, + }, + + /// A point in uncompressed form. + Uncompressed { + /// EC algorithm + algorithm: AlgorithmId, + + /// Encoded point + bytes: Buffer, + }, +} + /// Information about a public key within a [`Certificate`]. #[derive(Clone, Debug, Eq, PartialEq)] pub enum PublicKeyInfo { @@ -55,6 +77,9 @@ pub enum PublicKeyInfo { /// Public key pubkey: RSAPublicKey, }, + + /// EC keys + Ec(EcPoint), } impl PublicKeyInfo { @@ -73,6 +98,12 @@ impl PublicKeyInfo { pubkey, }) } + // EC Public Key + "1.2.840.10045.2.1" => { + let algorithm = read_pki::ec_parameters(&subject_pki.algorithm.parameters)?; + read_pki::ec_point(algorithm, subject_pki.subject_public_key.data) + .map(PublicKeyInfo::Ec) + } _ => Err(Error::InvalidObject), } } @@ -81,6 +112,10 @@ impl PublicKeyInfo { pub fn algorithm(&self) -> AlgorithmId { match self { PublicKeyInfo::Rsa { algorithm, .. } => *algorithm, + PublicKeyInfo::Ec(point) => match point { + EcPoint::Compressed { algorithm, .. } => *algorithm, + EcPoint::Uncompressed { algorithm, .. } => *algorithm, + }, } } } @@ -259,8 +294,10 @@ mod read_pki { }; use nom::{combinator, IResult}; use rsa::{BigUint, RSAPublicKey}; + use zeroize::Zeroizing; - use crate::error::Error; + use super::EcPoint; + use crate::{error::Error, key::AlgorithmId}; /// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1): /// ```text @@ -299,4 +336,79 @@ mod read_pki { RSAPublicKey::new(n, e).map_err(|_| Error::InvalidObject) } + + /// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1): + /// ```text + /// ECParameters ::= CHOICE { + /// namedCurve OBJECT IDENTIFIER + /// -- implicitCurve NULL + /// -- specifiedCurve SpecifiedECDomain + /// } + /// ``` + pub(super) fn ec_parameters(parameters: &DerObject<'_>) -> Result { + let curve_oid = match parameters.as_context_specific() { + Ok((_, Some(named_curve))) => { + named_curve.as_oid_val().map_err(|_| Error::InvalidObject) + } + _ => Err(Error::InvalidObject), + }?; + + match curve_oid.to_string().as_str() { + "1.2.840.10045.3.1.7" => Ok(AlgorithmId::EccP256), + "1.3.132.0.34" => Ok(AlgorithmId::EccP384), + _ => Err(Error::InvalidObject), + } + } + + /// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.2): + /// ```text + /// ECPoint ::= OCTET STRING + /// + /// o The first octet of the OCTET STRING indicates whether the key is + /// compressed or uncompressed. The uncompressed form is indicated + /// by 0x04 and the compressed form is indicated by either 0x02 or + /// 0x03 (see 2.3.3 in [SEC1]). The public key MUST be rejected if + /// any other value is included in the first octet. + /// ``` + pub(super) fn ec_point(algorithm: AlgorithmId, encoded: &[u8]) -> Result { + if encoded.is_empty() { + return Err(Error::InvalidObject); + } + + match encoded[0] { + 0x02 | 0x03 => { + if encoded.len() + == match algorithm { + AlgorithmId::EccP256 => 33, + AlgorithmId::EccP384 => 49, + _ => return Err(Error::AlgorithmError), + } + { + Ok(EcPoint::Compressed { + algorithm, + bytes: Zeroizing::new(encoded[1..].to_vec()), + }) + } else { + Err(Error::InvalidObject) + } + } + 0x04 => { + if encoded.len() + == match algorithm { + AlgorithmId::EccP256 => 65, + AlgorithmId::EccP384 => 97, + _ => return Err(Error::AlgorithmError), + } + { + Ok(EcPoint::Uncompressed { + algorithm, + bytes: Zeroizing::new(encoded[1..].to_vec()), + }) + } else { + Err(Error::InvalidObject) + } + } + _ => Err(Error::InvalidObject), + } + } } From 3a283aca4098d28cbd0cf78392f8e4073836b05f Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 Dec 2019 18:23:57 +0000 Subject: [PATCH 4/5] Use ecdsa crate for EC point representations --- Cargo.toml | 1 + src/certificate.rs | 147 ++++++++++++++++++++------------------------- 2 files changed, 66 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 796cdb9..b4ded39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ maintenance = { status = "experimental" } [dependencies] der-parser = "3" des = "0.3" +ecdsa = "0.1" getrandom = "0.1" hmac = "0.7" log = "0.4" diff --git a/src/certificate.rs b/src/certificate.rs index 7131fb4..63cbad8 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -39,35 +39,38 @@ use crate::{ yubikey::YubiKey, Buffer, }; +use ecdsa::{ + curve::{CompressedCurvePoint, NistP256, NistP384, UncompressedCurvePoint}, + generic_array::GenericArray, +}; use log::error; use rsa::{PublicKey, RSAPublicKey}; +use std::fmt; use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo}; use zeroize::Zeroizing; -/// An encoded point for some elliptic curve. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum EcPoint { - /// A point in compressed form. - Compressed { - /// EC algorithm - algorithm: AlgorithmId, +/// An encoded point on the Nist P-256 curve. +#[derive(Clone, Eq, PartialEq)] +pub enum EcP256Point { + /// Compressed encoding of a point on the curve. + Compressed(CompressedCurvePoint), - /// Encoded point - bytes: Buffer, - }, + /// Uncompressed encoding of a point on the curve. + Uncompressed(UncompressedCurvePoint), +} - /// A point in uncompressed form. - Uncompressed { - /// EC algorithm - algorithm: AlgorithmId, +/// An encoded point on the Nist P-384 curve. +#[derive(Clone, Eq, PartialEq)] +pub enum EcP384Point { + /// Compressed encoding of a point on the curve. + Compressed(CompressedCurvePoint), - /// Encoded point - bytes: Buffer, - }, + /// Uncompressed encoding of a point on the curve. + Uncompressed(UncompressedCurvePoint), } /// Information about a public key within a [`Certificate`]. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub enum PublicKeyInfo { /// RSA keys Rsa { @@ -78,8 +81,17 @@ pub enum PublicKeyInfo { pubkey: RSAPublicKey, }, - /// EC keys - Ec(EcPoint), + /// EC P-256 keys + EcP256(EcP256Point), + + /// EC P-384 keys + EcP384(EcP384Point), +} + +impl fmt::Debug for PublicKeyInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PublicKeyInfo({:?})", self.algorithm()) + } } impl PublicKeyInfo { @@ -100,9 +112,36 @@ impl PublicKeyInfo { } // EC Public Key "1.2.840.10045.2.1" => { - let algorithm = read_pki::ec_parameters(&subject_pki.algorithm.parameters)?; - read_pki::ec_point(algorithm, subject_pki.subject_public_key.data) - .map(PublicKeyInfo::Ec) + let key_bytes = &subject_pki.subject_public_key.data; + match read_pki::ec_parameters(&subject_pki.algorithm.parameters)? { + AlgorithmId::EccP256 => match key_bytes.len() { + 33 => CompressedCurvePoint::::from_bytes( + GenericArray::clone_from_slice(key_bytes), + ) + .map(EcP256Point::Compressed), + 65 => UncompressedCurvePoint::::from_bytes( + GenericArray::clone_from_slice(key_bytes), + ) + .map(EcP256Point::Uncompressed), + _ => None, + } + .map(PublicKeyInfo::EcP256) + .ok_or(Error::InvalidObject), + AlgorithmId::EccP384 => match key_bytes.len() { + 49 => CompressedCurvePoint::::from_bytes( + GenericArray::clone_from_slice(key_bytes), + ) + .map(EcP384Point::Compressed), + 97 => UncompressedCurvePoint::::from_bytes( + GenericArray::clone_from_slice(key_bytes), + ) + .map(EcP384Point::Uncompressed), + _ => None, + } + .map(PublicKeyInfo::EcP384) + .ok_or(Error::InvalidObject), + _ => Err(Error::AlgorithmError), + } } _ => Err(Error::InvalidObject), } @@ -112,10 +151,8 @@ impl PublicKeyInfo { pub fn algorithm(&self) -> AlgorithmId { match self { PublicKeyInfo::Rsa { algorithm, .. } => *algorithm, - PublicKeyInfo::Ec(point) => match point { - EcPoint::Compressed { algorithm, .. } => *algorithm, - EcPoint::Uncompressed { algorithm, .. } => *algorithm, - }, + PublicKeyInfo::EcP256(_) => AlgorithmId::EccP256, + PublicKeyInfo::EcP384(_) => AlgorithmId::EccP384, } } } @@ -294,9 +331,7 @@ mod read_pki { }; use nom::{combinator, IResult}; use rsa::{BigUint, RSAPublicKey}; - use zeroize::Zeroizing; - use super::EcPoint; use crate::{error::Error, key::AlgorithmId}; /// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1): @@ -356,59 +391,7 @@ mod read_pki { match curve_oid.to_string().as_str() { "1.2.840.10045.3.1.7" => Ok(AlgorithmId::EccP256), "1.3.132.0.34" => Ok(AlgorithmId::EccP384), - _ => Err(Error::InvalidObject), - } - } - - /// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.2): - /// ```text - /// ECPoint ::= OCTET STRING - /// - /// o The first octet of the OCTET STRING indicates whether the key is - /// compressed or uncompressed. The uncompressed form is indicated - /// by 0x04 and the compressed form is indicated by either 0x02 or - /// 0x03 (see 2.3.3 in [SEC1]). The public key MUST be rejected if - /// any other value is included in the first octet. - /// ``` - pub(super) fn ec_point(algorithm: AlgorithmId, encoded: &[u8]) -> Result { - if encoded.is_empty() { - return Err(Error::InvalidObject); - } - - match encoded[0] { - 0x02 | 0x03 => { - if encoded.len() - == match algorithm { - AlgorithmId::EccP256 => 33, - AlgorithmId::EccP384 => 49, - _ => return Err(Error::AlgorithmError), - } - { - Ok(EcPoint::Compressed { - algorithm, - bytes: Zeroizing::new(encoded[1..].to_vec()), - }) - } else { - Err(Error::InvalidObject) - } - } - 0x04 => { - if encoded.len() - == match algorithm { - AlgorithmId::EccP256 => 65, - AlgorithmId::EccP384 => 97, - _ => return Err(Error::AlgorithmError), - } - { - Ok(EcPoint::Uncompressed { - algorithm, - bytes: Zeroizing::new(encoded[1..].to_vec()), - }) - } else { - Err(Error::InvalidObject) - } - } - _ => Err(Error::InvalidObject), + _ => Err(Error::AlgorithmError), } } } From cd704c28d7e2c5906019f5183a926d076bee4803 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 Dec 2019 18:42:12 +0000 Subject: [PATCH 5/5] Extract OID strings as constants --- src/certificate.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/certificate.rs b/src/certificate.rs index 63cbad8..e11b303 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -49,6 +49,12 @@ use std::fmt; use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo}; use zeroize::Zeroizing; +// TODO: Make these der_parser::oid::Oid constants when it has const fn support. +const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1"; +const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1"; +const OID_NIST_P256: &str = "1.2.840.10045.3.1.7"; +const OID_NIST_P384: &str = "1.3.132.0.34"; + /// An encoded point on the Nist P-256 curve. #[derive(Clone, Eq, PartialEq)] pub enum EcP256Point { @@ -97,8 +103,7 @@ impl fmt::Debug for PublicKeyInfo { impl PublicKeyInfo { fn parse(subject_pki: &SubjectPublicKeyInfo<'_>) -> Result { match subject_pki.algorithm.algorithm.to_string().as_str() { - // RSA encryption - "1.2.840.113549.1.1.1" => { + OID_RSA_ENCRYPTION => { let pubkey = read_pki::rsa_pubkey(subject_pki.subject_public_key.data)?; Ok(PublicKeyInfo::Rsa { @@ -110,8 +115,7 @@ impl PublicKeyInfo { pubkey, }) } - // EC Public Key - "1.2.840.10045.2.1" => { + OID_EC_PUBLIC_KEY => { let key_bytes = &subject_pki.subject_public_key.data; match read_pki::ec_parameters(&subject_pki.algorithm.parameters)? { AlgorithmId::EccP256 => match key_bytes.len() { @@ -332,6 +336,7 @@ mod read_pki { use nom::{combinator, IResult}; use rsa::{BigUint, RSAPublicKey}; + use super::{OID_NIST_P256, OID_NIST_P384}; use crate::{error::Error, key::AlgorithmId}; /// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1): @@ -389,8 +394,8 @@ mod read_pki { }?; match curve_oid.to_string().as_str() { - "1.2.840.10045.3.1.7" => Ok(AlgorithmId::EccP256), - "1.3.132.0.34" => Ok(AlgorithmId::EccP384), + OID_NIST_P256 => Ok(AlgorithmId::EccP256), + OID_NIST_P384 => Ok(AlgorithmId::EccP384), _ => Err(Error::AlgorithmError), } }