Skip to content

Commit 8798e81

Browse files
authored
Add external types CI check + config (#183)
Having types from dependent crates appear in rcgen's public API makes taking semver incompatible updates to those dependencies tricky: we need to treat it as a semver incompatible change to rcgen itself. To make sure whenever this type leak happens that it was done because of a deliberate choice this branch adds [cargo-check-external-types](https://github.com/awslabs/cargo-check-external-types) to CI. Three existing types that leaked into the API are fixed: `ring::error::KeyRejected`, `ring::error::Unspecified`, and `pem::PemError`. In each case it's simple to translate the specific error types from the dependency into our own `Error` variants that don't expose the type. Two types are allow-listed: `time::offset_date_time::OffsetDateTime` and `zeroize::Zeroize`. The first, `OffsetDateTime` is used throughout the public API. It isn't clear to me if we want to keep that as part of the public API and treat `time` updates carefully, or if we should pursue using a more generic representation. I've left this out for now so it can be discussed. The second, `Zeroize`, could probably be avoided by implementing `Drop` and calling `zeroize` on the sensitive fields manually. That's the approach Rustls implemented (rustls/rustls#1492). I've left this out for in case there was a reason to prefer implementing the trait on the public types.
1 parent 1a579ca commit 8798e81

File tree

6 files changed

+138
-86
lines changed

6 files changed

+138
-86
lines changed

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,36 @@ jobs:
6161
env:
6262
RUSTDOCFLAGS: ${{ matrix.rust_channel == 'nightly' && '-Dwarnings --cfg=docsrs' || '-Dwarnings' }}
6363

64+
check-external-types:
65+
name: Validate external types appearing in public API
66+
runs-on: ubuntu-latest
67+
steps:
68+
- name: Checkout sources
69+
uses: actions/checkout@v4
70+
with:
71+
persist-credentials: false
72+
- name: Install rust toolchain
73+
uses: dtolnay/rust-toolchain@master
74+
with:
75+
toolchain: nightly-2023-10-10
76+
# ^ sync with https://github.com/awslabs/cargo-check-external-types/blob/main/rust-toolchain.toml
77+
- run: cargo install --locked cargo-check-external-types
78+
- name: run cargo-check-external-types for rcgen/
79+
working-directory: rcgen/
80+
run: cargo check-external-types --all-features
81+
82+
semver:
83+
name: Check semver compatibility
84+
runs-on: ubuntu-latest
85+
steps:
86+
- name: Checkout sources
87+
uses: actions/checkout@v4
88+
with:
89+
persist-credentials: false
90+
91+
- name: Check semver
92+
uses: obi1kenobi/cargo-semver-checks-action@v2
93+
6494
build-windows:
6595
runs-on: windows-latest
6696
env:

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rcgen/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rcgen"
3-
version = "0.11.3"
3+
version = "0.12.0"
44
documentation = "https://docs.rs/rcgen"
55
description.workspace = true
66
repository.workspace = true
@@ -35,6 +35,12 @@ default = ["pem"]
3535
[package.metadata.docs.rs]
3636
features = ["x509-parser"]
3737

38+
[package.metadata.cargo_check_external_types]
39+
allowed_external_types = [
40+
"time::offset_date_time::OffsetDateTime",
41+
"zeroize::Zeroize"
42+
]
43+
3844
[dev-dependencies]
3945
openssl = "0.10"
4046
x509-parser = { version = "0.15", features = ["verify"] }

rcgen/src/error.rs

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub enum Error {
3434
Time,
3535
#[cfg(feature = "pem")]
3636
/// Error from the pem crate
37-
PemError(pem::PemError),
37+
PemError(String),
3838
/// Error generated by a remote key operation
3939
RemoteKeyError,
4040
/// Unsupported field when generating a CSR
@@ -98,21 +98,10 @@ impl fmt::Display for Error {
9898

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

101-
impl From<ring::error::Unspecified> for Error {
102-
fn from(_unspecified: ring::error::Unspecified) -> Self {
103-
Error::RingUnspecified
104-
}
105-
}
106-
107-
impl From<ring::error::KeyRejected> for Error {
108-
fn from(err: ring::error::KeyRejected) -> Self {
109-
Error::RingKeyRejected(err.to_string())
110-
}
111-
}
112-
113-
#[cfg(feature = "pem")]
114-
impl From<pem::PemError> for Error {
115-
fn from(e: pem::PemError) -> Self {
116-
Error::PemError(e)
117-
}
101+
/// A trait describing an error that can be converted into an `rcgen::Error`.
102+
///
103+
/// We use this trait to avoid leaking external error types into the public API
104+
/// through a `From<x> for Error` implementation.
105+
pub(crate) trait ExternalError<T>: Sized {
106+
fn _err(self) -> Result<T, Error>;
118107
}

rcgen/src/key_pair.rs

Lines changed: 91 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::convert::TryFrom;
77
use std::fmt;
88
use yasna::DERWriter;
99

10+
use crate::error::ExternalError;
1011
use crate::sign_algo::algo::*;
1112
use crate::sign_algo::SignAlgo;
1213
#[cfg(feature = "pem")]
@@ -58,14 +59,16 @@ impl KeyPair {
5859
pub fn from_der(der: &[u8]) -> Result<Self, Error> {
5960
Ok(der.try_into()?)
6061
}
62+
6163
/// Returns the key pair's signature algorithm
6264
pub fn algorithm(&self) -> &'static SignatureAlgorithm {
6365
self.alg
6466
}
67+
6568
/// Parses the key pair from the ASCII PEM format
6669
#[cfg(feature = "pem")]
6770
pub fn from_pem(pem_str: &str) -> Result<Self, Error> {
68-
let private_key = pem::parse(pem_str)?;
71+
let private_key = pem::parse(pem_str)._err()?;
6972
let private_key_der: &[_] = private_key.contents();
7073
Ok(private_key_der.try_into()?)
7174
}
@@ -88,7 +91,7 @@ impl KeyPair {
8891
pem_str: &str,
8992
alg: &'static SignatureAlgorithm,
9093
) -> Result<Self, Error> {
91-
let private_key = pem::parse(pem_str)?;
94+
let private_key = pem::parse(pem_str)._err()?;
9295
let private_key_der: &[_] = private_key.contents();
9396
Ok(Self::from_der_and_sign_algo(private_key_der, alg)?)
9497
}
@@ -110,30 +113,28 @@ impl KeyPair {
110113
let pkcs8_vec = pkcs8.to_vec();
111114

112115
let kind = if alg == &PKCS_ED25519 {
113-
KeyPairKind::Ed(Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8)?)
116+
KeyPairKind::Ed(Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8)._err()?)
114117
} else if alg == &PKCS_ECDSA_P256_SHA256 {
115-
KeyPairKind::Ec(EcdsaKeyPair::from_pkcs8(
116-
&signature::ECDSA_P256_SHA256_ASN1_SIGNING,
117-
pkcs8,
118-
rng,
119-
)?)
118+
KeyPairKind::Ec(
119+
EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8, rng)
120+
._err()?,
121+
)
120122
} else if alg == &PKCS_ECDSA_P384_SHA384 {
121-
KeyPairKind::Ec(EcdsaKeyPair::from_pkcs8(
122-
&signature::ECDSA_P384_SHA384_ASN1_SIGNING,
123-
pkcs8,
124-
rng,
125-
)?)
123+
KeyPairKind::Ec(
124+
EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P384_SHA384_ASN1_SIGNING, pkcs8, rng)
125+
._err()?,
126+
)
126127
} else if alg == &PKCS_RSA_SHA256 {
127-
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
128+
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
128129
KeyPairKind::Rsa(rsakp, &signature::RSA_PKCS1_SHA256)
129130
} else if alg == &PKCS_RSA_SHA384 {
130-
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
131+
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
131132
KeyPairKind::Rsa(rsakp, &signature::RSA_PKCS1_SHA384)
132133
} else if alg == &PKCS_RSA_SHA512 {
133-
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
134+
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
134135
KeyPairKind::Rsa(rsakp, &signature::RSA_PKCS1_SHA512)
135136
} else if alg == &PKCS_RSA_PSS_SHA256 {
136-
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)?;
137+
let rsakp = RsaKeyPair::from_pkcs8(pkcs8)._err()?;
137138
KeyPairKind::Rsa(rsakp, &signature::RSA_PSS_SHA256)
138139
} else {
139140
panic!("Unknown SignatureAlgorithm specified!");
@@ -170,57 +171,14 @@ impl KeyPair {
170171
};
171172
Ok((kind, alg))
172173
}
173-
}
174-
175-
/// A private key that is not directly accessible, but can be used to sign messages
176-
///
177-
/// Trait objects based on this trait can be passed to the [`KeyPair::from_remote`] function for generating certificates
178-
/// from a remote and raw private key, for example an HSM.
179-
pub trait RemoteKeyPair {
180-
/// Returns the public key of this key pair in the binary format as in [`KeyPair::public_key_raw`]
181-
fn public_key(&self) -> &[u8];
182174

183-
/// Signs `msg` using the selected algorithm
184-
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error>;
185-
186-
/// Reveals the algorithm to be used when calling `sign()`
187-
fn algorithm(&self) -> &'static SignatureAlgorithm;
188-
}
189-
190-
impl TryFrom<&[u8]> for KeyPair {
191-
type Error = Error;
192-
193-
fn try_from(pkcs8: &[u8]) -> Result<KeyPair, Error> {
194-
let (kind, alg) = KeyPair::from_raw(pkcs8)?;
195-
Ok(KeyPair {
196-
kind,
197-
alg,
198-
serialized_der: pkcs8.to_vec(),
199-
})
200-
}
201-
}
202-
203-
impl TryFrom<Vec<u8>> for KeyPair {
204-
type Error = Error;
205-
206-
fn try_from(pkcs8: Vec<u8>) -> Result<KeyPair, Error> {
207-
let (kind, alg) = KeyPair::from_raw(pkcs8.as_slice())?;
208-
Ok(KeyPair {
209-
kind,
210-
alg,
211-
serialized_der: pkcs8,
212-
})
213-
}
214-
}
215-
216-
impl KeyPair {
217175
/// Generate a new random key pair for the specified signature algorithm
218176
pub fn generate(alg: &'static SignatureAlgorithm) -> Result<Self, Error> {
219177
let rng = &SystemRandom::new();
220178

221179
match alg.sign_alg {
222180
SignAlgo::EcDsa(sign_alg) => {
223-
let key_pair_doc = EcdsaKeyPair::generate_pkcs8(sign_alg, rng)?;
181+
let key_pair_doc = EcdsaKeyPair::generate_pkcs8(sign_alg, rng)._err()?;
224182
let key_pair_serialized = key_pair_doc.as_ref().to_vec();
225183

226184
let key_pair =
@@ -232,7 +190,7 @@ impl KeyPair {
232190
})
233191
},
234192
SignAlgo::EdDsa(_sign_alg) => {
235-
let key_pair_doc = Ed25519KeyPair::generate_pkcs8(rng)?;
193+
let key_pair_doc = Ed25519KeyPair::generate_pkcs8(rng)._err()?;
236194
let key_pair_serialized = key_pair_doc.as_ref().to_vec();
237195

238196
let key_pair = Ed25519KeyPair::from_pkcs8(&&key_pair_doc.as_ref()).unwrap();
@@ -248,6 +206,7 @@ impl KeyPair {
248206
SignAlgo::Rsa() => Err(Error::KeyGenerationUnavailable),
249207
}
250208
}
209+
251210
/// Get the raw public key of this key pair
252211
///
253212
/// The key is in raw format, as how [`ring::signature::KeyPair::public_key`]
@@ -256,20 +215,23 @@ impl KeyPair {
256215
pub fn public_key_raw(&self) -> &[u8] {
257216
self.raw_bytes()
258217
}
218+
259219
/// Check if this key pair can be used with the given signature algorithm
260220
pub fn is_compatible(&self, signature_algorithm: &SignatureAlgorithm) -> bool {
261221
self.alg == signature_algorithm
262222
}
223+
263224
/// Returns (possibly multiple) compatible [`SignatureAlgorithm`]'s
264225
/// that the key can be used with
265226
pub fn compatible_algs(&self) -> impl Iterator<Item = &'static SignatureAlgorithm> {
266227
std::iter::once(self.alg)
267228
}
229+
268230
pub(crate) fn sign(&self, msg: &[u8], writer: DERWriter) -> Result<(), Error> {
269231
match &self.kind {
270232
KeyPairKind::Ec(kp) => {
271233
let system_random = SystemRandom::new();
272-
let signature = kp.sign(&system_random, msg)?;
234+
let signature = kp.sign(&system_random, msg)._err()?;
273235
let sig = &signature.as_ref();
274236
writer.write_bitvec_bytes(&sig, &sig.len() * 8);
275237
},
@@ -281,7 +243,8 @@ impl KeyPair {
281243
KeyPairKind::Rsa(kp, padding_alg) => {
282244
let system_random = SystemRandom::new();
283245
let mut signature = vec![0; kp.public().modulus_len()];
284-
kp.sign(*padding_alg, &system_random, msg, &mut signature)?;
246+
kp.sign(*padding_alg, &system_random, msg, &mut signature)
247+
._err()?;
285248
let sig = &signature.as_ref();
286249
writer.write_bitvec_bytes(&sig, &sig.len() * 8);
287250
},
@@ -292,6 +255,7 @@ impl KeyPair {
292255
}
293256
Ok(())
294257
}
258+
295259
/// Return the key pair's public key in DER format
296260
///
297261
/// The key is formatted according to the SubjectPublicKeyInfo struct of
@@ -300,6 +264,7 @@ impl KeyPair {
300264
pub fn public_key_der(&self) -> Vec<u8> {
301265
yasna::construct_der(|writer| self.serialize_public_key_der(writer))
302266
}
267+
303268
/// Return the key pair's public key in PEM format
304269
///
305270
/// The returned string can be interpreted with `openssl pkey --inform PEM -pubout -pubin -text`
@@ -309,6 +274,7 @@ impl KeyPair {
309274
let p = Pem::new("PUBLIC KEY", contents);
310275
pem::encode_config(&p, ENCODE_CONFIG)
311276
}
277+
312278
/// Serializes the key pair (including the private key) in PKCS#8 format in DER
313279
///
314280
/// Panics if called on a remote key pair.
@@ -350,6 +316,32 @@ impl KeyPair {
350316
}
351317
}
352318

319+
impl TryFrom<&[u8]> for KeyPair {
320+
type Error = Error;
321+
322+
fn try_from(pkcs8: &[u8]) -> Result<KeyPair, Error> {
323+
let (kind, alg) = KeyPair::from_raw(pkcs8)?;
324+
Ok(KeyPair {
325+
kind,
326+
alg,
327+
serialized_der: pkcs8.to_vec(),
328+
})
329+
}
330+
}
331+
332+
impl TryFrom<Vec<u8>> for KeyPair {
333+
type Error = Error;
334+
335+
fn try_from(pkcs8: Vec<u8>) -> Result<KeyPair, Error> {
336+
let (kind, alg) = KeyPair::from_raw(pkcs8.as_slice())?;
337+
Ok(KeyPair {
338+
kind,
339+
alg,
340+
serialized_der: pkcs8,
341+
})
342+
}
343+
}
344+
353345
impl PublicKeyData for KeyPair {
354346
fn alg(&self) -> &SignatureAlgorithm {
355347
self.alg
@@ -364,9 +356,44 @@ impl PublicKeyData for KeyPair {
364356
}
365357
}
366358

359+
/// A private key that is not directly accessible, but can be used to sign messages
360+
///
361+
/// Trait objects based on this trait can be passed to the [`KeyPair::from_remote`] function for generating certificates
362+
/// from a remote and raw private key, for example an HSM.
363+
pub trait RemoteKeyPair {
364+
/// Returns the public key of this key pair in the binary format as in [`KeyPair::public_key_raw`]
365+
fn public_key(&self) -> &[u8];
366+
367+
/// Signs `msg` using the selected algorithm
368+
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error>;
369+
370+
/// Reveals the algorithm to be used when calling `sign()`
371+
fn algorithm(&self) -> &'static SignatureAlgorithm;
372+
}
373+
374+
impl<T> ExternalError<T> for Result<T, ring::error::KeyRejected> {
375+
fn _err(self) -> Result<T, Error> {
376+
self.map_err(|e| Error::RingKeyRejected(e.to_string()))
377+
}
378+
}
379+
380+
impl<T> ExternalError<T> for Result<T, ring::error::Unspecified> {
381+
fn _err(self) -> Result<T, Error> {
382+
self.map_err(|_| Error::RingUnspecified)
383+
}
384+
}
385+
386+
impl<T> ExternalError<T> for Result<T, pem::PemError> {
387+
fn _err(self) -> Result<T, Error> {
388+
self.map_err(|e| Error::PemError(e.to_string()))
389+
}
390+
}
391+
367392
pub(crate) trait PublicKeyData {
368393
fn alg(&self) -> &SignatureAlgorithm;
394+
369395
fn raw_bytes(&self) -> &[u8];
396+
370397
fn serialize_public_key_der(&self, writer: DERWriter) {
371398
writer.write_sequence(|writer| {
372399
self.alg().write_oids_sign_alg(writer.next());

0 commit comments

Comments
 (0)