Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add external types CI check + config #183

Merged
merged 10 commits into from
Dec 9, 2023
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@ jobs:
env:
RUSTDOCFLAGS: ${{ matrix.rust_channel == 'nightly' && '-Dwarnings --cfg=docsrs' || '-Dwarnings' }}

check-external-types:
name: Validate external types appearing in public API
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-10-10
# ^ sync with https://github.com/awslabs/cargo-check-external-types/blob/main/rust-toolchain.toml
- run: cargo install --locked cargo-check-external-types
cpu marked this conversation as resolved.
Show resolved Hide resolved
- name: run cargo-check-external-types for rcgen/
working-directory: rcgen/
run: cargo check-external-types --all-features

semver:
name: Check semver compatibility
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2
cpu marked this conversation as resolved.
Show resolved Hide resolved

build-windows:
runs-on: windows-latest
env:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

8 changes: 7 additions & 1 deletion rcgen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rcgen"
version = "0.11.3"
version = "0.12.0"
documentation = "https://docs.rs/rcgen"
description.workspace = true
repository.workspace = true
Expand Down Expand Up @@ -35,6 +35,12 @@ default = ["pem"]
[package.metadata.docs.rs]
features = ["x509-parser"]

[package.metadata.cargo_check_external_types]
allowed_external_types = [
"time::offset_date_time::OffsetDateTime",
"zeroize::Zeroize"
]

[dev-dependencies]
openssl = "0.10"
x509-parser = { version = "0.15", features = ["verify"] }
Expand Down
25 changes: 7 additions & 18 deletions rcgen/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub enum Error {
Time,
#[cfg(feature = "pem")]
/// Error from the pem crate
PemError(pem::PemError),
PemError(String),
/// Error generated by a remote key operation
RemoteKeyError,
/// Unsupported field when generating a CSR
Expand Down Expand Up @@ -98,21 +98,10 @@ impl fmt::Display for Error {

impl std::error::Error for Error {}

impl From<ring::error::Unspecified> for Error {
fn from(_unspecified: ring::error::Unspecified) -> Self {
Error::RingUnspecified
}
}

impl From<ring::error::KeyRejected> for Error {
fn from(err: ring::error::KeyRejected) -> Self {
Error::RingKeyRejected(err.to_string())
}
}

#[cfg(feature = "pem")]
impl From<pem::PemError> for Error {
fn from(e: pem::PemError) -> Self {
Error::PemError(e)
}
/// A trait describing an error that can be converted into an `rcgen::Error`.
///
/// We use this trait to avoid leaking external error types into the public API
/// through a `From<x> for Error` implementation.
pub(crate) trait ExternalError<T>: Sized {
fn _err(self) -> Result<T, Error>;
}
155 changes: 91 additions & 64 deletions rcgen/src/key_pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use std::fmt;
use yasna::DERWriter;

use crate::error::ExternalError;
use crate::sign_algo::algo::*;
use crate::sign_algo::SignAlgo;
#[cfg(feature = "pem")]
Expand Down Expand Up @@ -58,14 +59,16 @@
pub fn from_der(der: &[u8]) -> Result<Self, Error> {
Ok(der.try_into()?)
}

/// Returns the key pair's signature algorithm
pub fn algorithm(&self) -> &'static SignatureAlgorithm {
self.alg
}

/// Parses the key pair from the ASCII PEM format
#[cfg(feature = "pem")]
pub fn from_pem(pem_str: &str) -> Result<Self, Error> {
let private_key = pem::parse(pem_str)?;
let private_key = pem::parse(pem_str)._err()?;
let private_key_der: &[_] = private_key.contents();
Ok(private_key_der.try_into()?)
}
Expand All @@ -88,7 +91,7 @@
pem_str: &str,
alg: &'static SignatureAlgorithm,
) -> Result<Self, Error> {
let private_key = pem::parse(pem_str)?;
let private_key = pem::parse(pem_str)._err()?;
let private_key_der: &[_] = private_key.contents();
Ok(Self::from_der_and_sign_algo(private_key_der, alg)?)
}
Expand All @@ -110,30 +113,28 @@
let pkcs8_vec = pkcs8.to_vec();

let kind = if alg == &PKCS_ED25519 {
KeyPairKind::Ed(Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8)?)
KeyPairKind::Ed(Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8)._err()?)

Check warning on line 116 in rcgen/src/key_pair.rs

View check run for this annotation

Codecov / codecov/patch

rcgen/src/key_pair.rs#L116

Added line #L116 was not covered by tests
} else if alg == &PKCS_ECDSA_P256_SHA256 {
KeyPairKind::Ec(EcdsaKeyPair::from_pkcs8(
&signature::ECDSA_P256_SHA256_ASN1_SIGNING,
pkcs8,
rng,
)?)
KeyPairKind::Ec(
EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8, rng)
._err()?,

Check warning on line 120 in rcgen/src/key_pair.rs

View check run for this annotation

Codecov / codecov/patch

rcgen/src/key_pair.rs#L119-L120

Added lines #L119 - L120 were not covered by tests
)
} else if alg == &PKCS_ECDSA_P384_SHA384 {
KeyPairKind::Ec(EcdsaKeyPair::from_pkcs8(
&signature::ECDSA_P384_SHA384_ASN1_SIGNING,
pkcs8,
rng,
)?)
KeyPairKind::Ec(
EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P384_SHA384_ASN1_SIGNING, pkcs8, rng)
._err()?,

Check warning on line 125 in rcgen/src/key_pair.rs

View check run for this annotation

Codecov / codecov/patch

rcgen/src/key_pair.rs#L124-L125

Added lines #L124 - L125 were not covered by tests
)
} else if alg == &PKCS_RSA_SHA256 {
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
KeyPairKind::Rsa(rsakp, &signature::RSA_PKCS1_SHA256)
} else if alg == &PKCS_RSA_SHA384 {
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
KeyPairKind::Rsa(rsakp, &signature::RSA_PKCS1_SHA384)
} else if alg == &PKCS_RSA_SHA512 {
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
KeyPairKind::Rsa(rsakp, &signature::RSA_PKCS1_SHA512)
} else if alg == &PKCS_RSA_PSS_SHA256 {
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;

Check warning on line 137 in rcgen/src/key_pair.rs

View check run for this annotation

Codecov / codecov/patch

rcgen/src/key_pair.rs#L137

Added line #L137 was not covered by tests
KeyPairKind::Rsa(rsakp, &signature::RSA_PSS_SHA256)
} else {
panic!("Unknown SignatureAlgorithm specified!");
Expand Down Expand Up @@ -170,57 +171,14 @@
};
Ok((kind, alg))
}
}

/// A private key that is not directly accessible, but can be used to sign messages
///
/// Trait objects based on this trait can be passed to the [`KeyPair::from_remote`] function for generating certificates
/// from a remote and raw private key, for example an HSM.
pub trait RemoteKeyPair {
/// Returns the public key of this key pair in the binary format as in [`KeyPair::public_key_raw`]
fn public_key(&self) -> &[u8];

/// Signs `msg` using the selected algorithm
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error>;

/// Reveals the algorithm to be used when calling `sign()`
fn algorithm(&self) -> &'static SignatureAlgorithm;
}

impl TryFrom<&[u8]> for KeyPair {
type Error = Error;

fn try_from(pkcs8: &[u8]) -> Result<KeyPair, Error> {
let (kind, alg) = KeyPair::from_raw(pkcs8)?;
Ok(KeyPair {
kind,
alg,
serialized_der: pkcs8.to_vec(),
})
}
}

impl TryFrom<Vec<u8>> for KeyPair {
type Error = Error;

fn try_from(pkcs8: Vec<u8>) -> Result<KeyPair, Error> {
let (kind, alg) = KeyPair::from_raw(pkcs8.as_slice())?;
Ok(KeyPair {
kind,
alg,
serialized_der: pkcs8,
})
}
}

impl KeyPair {
/// Generate a new random key pair for the specified signature algorithm
pub fn generate(alg: &'static SignatureAlgorithm) -> Result<Self, Error> {
let rng = &SystemRandom::new();

match alg.sign_alg {
SignAlgo::EcDsa(sign_alg) => {
let key_pair_doc = EcdsaKeyPair::generate_pkcs8(sign_alg, rng)?;
let key_pair_doc = EcdsaKeyPair::generate_pkcs8(sign_alg, rng)._err()?;
let key_pair_serialized = key_pair_doc.as_ref().to_vec();

let key_pair =
Expand All @@ -232,7 +190,7 @@
})
},
SignAlgo::EdDsa(_sign_alg) => {
let key_pair_doc = Ed25519KeyPair::generate_pkcs8(rng)?;
let key_pair_doc = Ed25519KeyPair::generate_pkcs8(rng)._err()?;
let key_pair_serialized = key_pair_doc.as_ref().to_vec();

let key_pair = Ed25519KeyPair::from_pkcs8(&&key_pair_doc.as_ref()).unwrap();
Expand All @@ -248,6 +206,7 @@
SignAlgo::Rsa() => Err(Error::KeyGenerationUnavailable),
}
}

/// Get the raw public key of this key pair
///
/// The key is in raw format, as how [`ring::signature::KeyPair::public_key`]
Expand All @@ -256,20 +215,23 @@
pub fn public_key_raw(&self) -> &[u8] {
self.raw_bytes()
}

/// Check if this key pair can be used with the given signature algorithm
pub fn is_compatible(&self, signature_algorithm: &SignatureAlgorithm) -> bool {
self.alg == signature_algorithm
}

/// Returns (possibly multiple) compatible [`SignatureAlgorithm`]'s
/// that the key can be used with
pub fn compatible_algs(&self) -> impl Iterator<Item = &'static SignatureAlgorithm> {
std::iter::once(self.alg)
}

pub(crate) fn sign(&self, msg: &[u8], writer: DERWriter) -> Result<(), Error> {
match &self.kind {
KeyPairKind::Ec(kp) => {
let system_random = SystemRandom::new();
let signature = kp.sign(&system_random, msg)?;
let signature = kp.sign(&system_random, msg)._err()?;
let sig = &signature.as_ref();
writer.write_bitvec_bytes(&sig, &sig.len() * 8);
},
Expand All @@ -281,7 +243,8 @@
KeyPairKind::Rsa(kp, padding_alg) => {
let system_random = SystemRandom::new();
let mut signature = vec![0; kp.public().modulus_len()];
kp.sign(*padding_alg, &system_random, msg, &mut signature)?;
kp.sign(*padding_alg, &system_random, msg, &mut signature)
._err()?;
let sig = &signature.as_ref();
writer.write_bitvec_bytes(&sig, &sig.len() * 8);
},
Expand All @@ -292,6 +255,7 @@
}
Ok(())
}

/// Return the key pair's public key in DER format
///
/// The key is formatted according to the SubjectPublicKeyInfo struct of
Expand All @@ -300,6 +264,7 @@
pub fn public_key_der(&self) -> Vec<u8> {
yasna::construct_der(|writer| self.serialize_public_key_der(writer))
}

/// Return the key pair's public key in PEM format
///
/// The returned string can be interpreted with `openssl pkey --inform PEM -pubout -pubin -text`
Expand All @@ -309,6 +274,7 @@
let p = Pem::new("PUBLIC KEY", contents);
pem::encode_config(&p, ENCODE_CONFIG)
}

/// Serializes the key pair (including the private key) in PKCS#8 format in DER
///
/// Panics if called on a remote key pair.
Expand Down Expand Up @@ -350,6 +316,32 @@
}
}

impl TryFrom<&[u8]> for KeyPair {
type Error = Error;

fn try_from(pkcs8: &[u8]) -> Result<KeyPair, Error> {
let (kind, alg) = KeyPair::from_raw(pkcs8)?;
Ok(KeyPair {
kind,
alg,
serialized_der: pkcs8.to_vec(),
})
}
}

impl TryFrom<Vec<u8>> for KeyPair {
type Error = Error;

fn try_from(pkcs8: Vec<u8>) -> Result<KeyPair, Error> {
let (kind, alg) = KeyPair::from_raw(pkcs8.as_slice())?;
Ok(KeyPair {
kind,
alg,
serialized_der: pkcs8,
})
}

Check warning on line 342 in rcgen/src/key_pair.rs

View check run for this annotation

Codecov / codecov/patch

rcgen/src/key_pair.rs#L335-L342

Added lines #L335 - L342 were not covered by tests
}

impl PublicKeyData for KeyPair {
fn alg(&self) -> &SignatureAlgorithm {
self.alg
Expand All @@ -364,9 +356,44 @@
}
}

/// A private key that is not directly accessible, but can be used to sign messages
///
/// Trait objects based on this trait can be passed to the [`KeyPair::from_remote`] function for generating certificates
/// from a remote and raw private key, for example an HSM.
pub trait RemoteKeyPair {
/// Returns the public key of this key pair in the binary format as in [`KeyPair::public_key_raw`]
fn public_key(&self) -> &[u8];

/// Signs `msg` using the selected algorithm
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error>;

/// Reveals the algorithm to be used when calling `sign()`
fn algorithm(&self) -> &'static SignatureAlgorithm;
}

impl<T> ExternalError<T> for Result<T, ring::error::KeyRejected> {
fn _err(self) -> Result<T, Error> {
self.map_err(|e| Error::RingKeyRejected(e.to_string()))
}
}

impl<T> ExternalError<T> for Result<T, ring::error::Unspecified> {
fn _err(self) -> Result<T, Error> {
self.map_err(|_| Error::RingUnspecified)
}
}

impl<T> ExternalError<T> for Result<T, pem::PemError> {
fn _err(self) -> Result<T, Error> {
self.map_err(|e| Error::PemError(e.to_string()))
}
}

pub(crate) trait PublicKeyData {
fn alg(&self) -> &SignatureAlgorithm;

fn raw_bytes(&self) -> &[u8];

fn serialize_public_key_der(&self, writer: DERWriter) {
writer.write_sequence(|writer| {
self.alg().write_oids_sign_alg(writer.next());
Expand Down