-
-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathmod.rs
More file actions
574 lines (500 loc) · 21.3 KB
/
mod.rs
File metadata and controls
574 lines (500 loc) · 21.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
//! Contains types and functions to generate and sign certificate authorities
//! (CAs).
use std::{fmt::Debug, str::FromStr};
use const_oid::db::rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH};
use k8s_openapi::api::core::v1::Secret;
use kube::{Api, Client, runtime::reflector::ObjectRef};
use snafu::{OptionExt, ResultExt, Snafu};
use stackable_shared::{secret::SecretReference, time::Duration};
use tracing::{debug, instrument};
use x509_cert::{
Certificate,
builder::{Builder, CertificateBuilder, Profile},
der::{DecodePem, asn1::Ia5String, pem::LineEnding, referenced::OwnedToRef},
ext::pkix::{AuthorityKeyIdentifier, ExtendedKeyUsage, SubjectAltName, name::GeneralName},
name::Name,
serial_number::SerialNumber,
spki::{EncodePublicKey, SubjectPublicKeyInfoOwned},
time::Validity,
};
use crate::{
CertificatePair,
keys::{CertificateKeypair, ecdsa, rsa},
};
mod consts;
pub use consts::*;
pub const TLS_SECRET_TYPE: &str = "kubernetes.io/tls";
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Defines all error variants which can occur when creating a CA and/or leaf
/// certificates.
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("failed to generate RSA signing key"))]
GenerateRsaSigningKey { source: rsa::Error },
#[snafu(display("failed to generate ECDSA signing key"))]
GenerateEcdsaSigningKey { source: ecdsa::Error },
#[snafu(display("failed to parse {subject:?} as subject"))]
ParseSubject {
source: x509_cert::der::Error,
subject: String,
},
#[snafu(display("failed to parse validity"))]
ParseValidity { source: x509_cert::der::Error },
#[snafu(display("failed to serialize public key as PEM"))]
SerializePublicKey { source: x509_cert::spki::Error },
#[snafu(display("failed to decode SPKI from PEM"))]
DecodeSpkiFromPem { source: x509_cert::der::Error },
#[snafu(display("failed to create certificate builder"))]
CreateCertificateBuilder { source: x509_cert::builder::Error },
#[snafu(display("failed to add certificate extension"))]
AddCertificateExtension { source: x509_cert::builder::Error },
#[snafu(display("failed to build certificate"))]
BuildCertificate { source: x509_cert::builder::Error },
#[snafu(display("failed to parse AuthorityKeyIdentifier"))]
ParseAuthorityKeyIdentifier { source: x509_cert::der::Error },
#[snafu(display(
"failed to parse subject alternative DNS name {subject_alternative_dns_name:?} as a Ia5 string"
))]
ParseSubjectAlternativeDnsName {
subject_alternative_dns_name: String,
source: x509_cert::der::Error,
},
}
/// Custom implementation of [`std::cmp::PartialEq`] because some inner types
/// don't implement it.
///
/// Note that this implementation is restricted to testing because there is a
/// variant that is impossible to compare, and will cause a panic if it is
/// attempted.
#[cfg(test)]
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::CreateCertificateBuilder { source: lhs_source },
Self::CreateCertificateBuilder { source: rhs_source },
)
| (
Self::AddCertificateExtension { source: lhs_source },
Self::AddCertificateExtension { source: rhs_source },
)
| (
Self::BuildCertificate { source: lhs_source },
Self::BuildCertificate { source: rhs_source },
) => match (lhs_source, rhs_source) {
(x509_cert::builder::Error::Asn1(lhs), x509_cert::builder::Error::Asn1(rhs)) => {
lhs == rhs
}
(
x509_cert::builder::Error::PublicKey(lhs),
x509_cert::builder::Error::PublicKey(rhs),
) => lhs == rhs,
(
x509_cert::builder::Error::Signature(_),
x509_cert::builder::Error::Signature(_),
) => panic!(
"it is impossible to compare the opaque Error contained within signature::error::Error"
),
_ => false,
},
(lhs, rhs) => lhs == rhs,
}
}
}
/// Defines all error variants which can occur when loading a CA from a
/// Kubernetes [`Secret`].
#[derive(Debug, Snafu)]
pub enum SecretError<E>
where
E: std::error::Error + 'static,
{
#[snafu(display("failed to retrieve secret \"{secret_ref}\""))]
GetSecret {
#[snafu(source(from(kube::Error, Box::new)))]
source: Box<kube::Error>,
secret_ref: SecretReference,
},
#[snafu(display("invalid secret type, expected {TLS_SECRET_TYPE}"))]
InvalidSecretType,
#[snafu(display("the secret {secret:?} does not contain any data"))]
NoSecretData { secret: ObjectRef<Secret> },
#[snafu(display("the secret {secret:?} does not contain TLS certificate data"))]
NoCertificateData { secret: ObjectRef<Secret> },
#[snafu(display("the secret {secret:?} does not contain TLS private key data"))]
NoPrivateKeyData { secret: ObjectRef<Secret> },
#[snafu(display("failed to read PEM-encoded certificate chain from secret {secret:?}"))]
ReadChain {
#[snafu(source(from(x509_cert::der::Error, Box::new)))]
source: Box<x509_cert::der::Error>,
secret: ObjectRef<Secret>,
},
#[snafu(display("failed to parse UTF-8 encoded byte string"))]
DecodeUtf8String { source: std::str::Utf8Error },
#[snafu(display("failed to deserialize private key from PEM"))]
DeserializeKeyFromPem {
#[snafu(source(from(E, Box::new)))]
source: Box<E>,
},
}
/// A certificate authority (CA) which is used to generate and sign
/// intermediate or leaf certificates.
#[derive(Debug)]
pub struct CertificateAuthority<S>
where
S: CertificateKeypair,
<S::SigningKey as signature::Keypair>::VerifyingKey: EncodePublicKey,
{
certificate_pair: CertificatePair<S>,
}
impl<S> CertificateAuthority<S>
where
S: CertificateKeypair,
<S::SigningKey as signature::Keypair>::VerifyingKey: EncodePublicKey,
{
/// Creates a new CA certificate with many parameters set to their default
/// values.
///
/// These parameters include:
///
/// - a randomly generated serial number
/// - a default validity of one hour (see [`DEFAULT_CA_VALIDITY`])
///
/// The CA contains the public half of the provided `signing_key` and is
/// signed by the private half of said key.
///
/// If the default values for the serial number and validity don't satisfy
/// the requirements of the caller, use [`CertificateAuthority::new_with`]
/// instead.
#[instrument(name = "create_certificate_authority", skip(signing_key_pair))]
pub fn new(signing_key_pair: S) -> Result<Self> {
let serial_number = rand::random::<u64>();
Self::new_with(signing_key_pair, serial_number, DEFAULT_CA_VALIDITY)
}
/// Creates a new CA certificate.
///
/// Instead of providing sensible defaults for the serial number and
/// validity, this function offers complete control over these parameters.
/// If this level of control is not needed, use [`CertificateAuthority::new`]
/// instead.
//
// SAFETY: We purposefully allow the `clippy::unwrap_in_result` lint below in this function.
// We can use expect here, because the subject name is defined as a constant which must be able
// to be parsed.
//
// FIXME (@Techassi): This attribute can be used on individual unwrap and expect calls since
// Rust 1.91.0. We should move this attribute to not contaminate an unnecessarily large scope
// once we bump the toolchain to 1.91.0.
// See https://github.com/rust-lang/rust-clippy/pull/15445
#[allow(clippy::unwrap_in_result)]
#[instrument(name = "create_certificate_authority_with", skip(signing_key_pair))]
pub fn new_with(signing_key_pair: S, serial_number: u64, validity: Duration) -> Result<Self> {
let serial_number = SerialNumber::from(serial_number);
let validity = Validity::from_now(*validity).context(ParseValiditySnafu)?;
// We don't allow customization of the CA subject by callers. Every CA
// created by us should contain the same subject consisting a common set
// of distinguished names (DNs).
let subject = Name::from_str(SDP_ROOT_CA_SUBJECT)
.expect("the constant SDP_ROOT_CA_SUBJECT must be a valid subject");
let spki_pem = signing_key_pair
.verifying_key()
.to_public_key_pem(LineEnding::LF)
.context(SerializePublicKeySnafu)?;
let spki = SubjectPublicKeyInfoOwned::from_pem(spki_pem.as_bytes())
.context(DecodeSpkiFromPemSnafu)?;
// There are multiple default extensions included in the profile. For
// the root profile, these are:
//
// - BasicConstraints marked as critical and CA = true
// - SubjectKeyIdentifier with the 160-bit SHA-1 hash of the subject
// public key.
// - KeyUsage with KeyCertSign and CRLSign bits set. Ideally we also
// want to include the DigitalSignature bit, which for example is
// required for CA certs which want to sign an OCSP response.
// Currently, the root profile doesn't include that bit.
//
// The root profile doesn't add the AuthorityKeyIdentifier extension.
// We manually add it below by using the 160-bit SHA-1 hash of the
// subject public key. This conforms to one of the outlined methods for
// generating key identifiers outlined in RFC 5280, section 4.2.1.2.
//
// Prepare extensions so we can avoid clones.
let aki = AuthorityKeyIdentifier::try_from(spki.owned_to_ref())
.context(ParseAuthorityKeyIdentifierSnafu)?;
let signer = signing_key_pair.signing_key();
let mut builder = CertificateBuilder::new(
Profile::Root,
serial_number,
validity,
subject,
spki,
signer,
)
.context(CreateCertificateBuilderSnafu)?;
// Add extension constructed above
builder
.add_extension(&aki)
.context(AddCertificateExtensionSnafu)?;
debug!("create and sign CA certificate");
let certificate = builder.build().context(BuildCertificateSnafu)?;
Ok(Self {
certificate_pair: CertificatePair {
key_pair: signing_key_pair,
certificate,
},
})
}
/// Generates a leaf certificate which is signed by this CA.
///
/// The certificate requires a `name` and a `scope`. Both these values
/// are part of the certificate subject. The format is: `{name} Certificate
/// for {scope}`. These leaf certificates can be used for client/server
/// authentication, because they include [`ID_KP_CLIENT_AUTH`] and
/// [`ID_KP_SERVER_AUTH`] in the extended key usage extension.
///
/// It is also possible to directly create RSA or ECDSA-based leaf
/// certificates using [`CertificateAuthority::generate_rsa_leaf_certificate`]
/// and [`CertificateAuthority::generate_ecdsa_leaf_certificate`].
#[instrument(skip(self, key_pair))]
pub fn generate_leaf_certificate<'a, T>(
&mut self,
key_pair: T,
name: &str,
scope: &str,
subject_alternative_dns_names: impl IntoIterator<Item = &'a str> + Debug,
validity: Duration,
) -> Result<CertificatePair<T>>
where
T: CertificateKeypair,
<T::SigningKey as signature::Keypair>::VerifyingKey: EncodePublicKey,
{
// We generate a random serial number, but ensure the same CA didn't
// issue another certificate with the same serial number. We try to
// generate a unique serial number at max five times before giving up
// and returning an error.
let serial_number = SerialNumber::from(rand::random::<u64>());
// NOTE (@Techassi): Should we validate that the validity is shorter
// than the validity of the issuing CA?
let validity = Validity::from_now(*validity).context(ParseValiditySnafu)?;
let subject = format_leaf_certificate_subject(name, scope)?;
let spki_pem = key_pair
.verifying_key()
.to_public_key_pem(LineEnding::LF)
.context(SerializePublicKeySnafu)?;
let spki = SubjectPublicKeyInfoOwned::from_pem(spki_pem.as_bytes())
.context(DecodeSpkiFromPemSnafu)?;
let signer = self.certificate_pair.key_pair.signing_key();
let mut builder = CertificateBuilder::new(
Profile::Leaf {
issuer: self
.certificate_pair
.certificate
.tbs_certificate
.issuer
.clone(),
enable_key_agreement: false,
enable_key_encipherment: true,
},
serial_number,
validity,
subject,
spki,
signer,
)
.context(CreateCertificateBuilderSnafu)?;
// The leaf certificate can be used for WWW client and server
// authentication. This is a base requirement for TLS certs.
builder
.add_extension(&ExtendedKeyUsage(vec![
ID_KP_CLIENT_AUTH,
ID_KP_SERVER_AUTH,
]))
.context(AddCertificateExtensionSnafu)?;
let sans = subject_alternative_dns_names
.into_iter()
.map(|dns_name| {
let ia5_dns_name =
Ia5String::new(dns_name).context(ParseSubjectAlternativeDnsNameSnafu {
subject_alternative_dns_name: dns_name.to_string(),
})?;
Ok(GeneralName::DnsName(ia5_dns_name))
})
.collect::<Result<Vec<_>, Error>>()?;
builder
.add_extension(&SubjectAltName(sans))
.context(AddCertificateExtensionSnafu)?;
debug!("create and sign leaf certificate");
let certificate = builder.build().context(BuildCertificateSnafu)?;
Ok(CertificatePair {
certificate,
key_pair,
})
}
/// Generates an RSA-based leaf certificate which is signed by this CA.
///
/// See [`CertificateAuthority::generate_leaf_certificate`] for more
/// information.
#[instrument(skip(self))]
pub fn generate_rsa_leaf_certificate<'a>(
&mut self,
name: &str,
scope: &str,
subject_alternative_dns_names: impl IntoIterator<Item = &'a str> + Debug,
validity: Duration,
) -> Result<CertificatePair<rsa::SigningKey>> {
let key = rsa::SigningKey::new().context(GenerateRsaSigningKeySnafu)?;
self.generate_leaf_certificate(key, name, scope, subject_alternative_dns_names, validity)
}
/// Generates an ECDSAasync -based leaf certificate which is signed by this CA.
///
/// See [`CertificateAuthority::generate_leaf_certificate`] for more
/// information.
#[instrument(skip(self))]
pub fn generate_ecdsa_leaf_certificate<'a>(
&mut self,
name: &str,
scope: &str,
subject_alternative_dns_names: impl IntoIterator<Item = &'a str> + Debug,
validity: Duration,
) -> Result<CertificatePair<ecdsa::SigningKey>> {
let key = ecdsa::SigningKey::new().context(GenerateEcdsaSigningKeySnafu)?;
self.generate_leaf_certificate(key, name, scope, subject_alternative_dns_names, validity)
}
/// Create a [`CertificateAuthority`] from a Kubernetes [`Secret`].
///
/// Both the `key_certificate` and `key_private_key` parameters describe
/// the _key_ used to lookup the certificate and private key value in the
/// Kubernetes [`Secret`]. Common keys are `ca.crt` and `ca.key`.
#[instrument(name = "create_certificate_authority_from_k8s_secret", skip(secret))]
pub fn from_secret(
secret: Secret,
key_certificate: &str,
key_private_key: &str,
) -> Result<Self, SecretError<S::Error>> {
if secret.type_.as_ref().is_none_or(|s| s != TLS_SECRET_TYPE) {
return InvalidSecretTypeSnafu.fail();
}
let data = secret.data.as_ref().with_context(|| NoSecretDataSnafu {
secret: ObjectRef::from_obj(&secret),
})?;
debug!("retrieving certificate data from secret via key {key_certificate:?}");
let certificate_data =
data.get(key_certificate)
.with_context(|| NoCertificateDataSnafu {
secret: ObjectRef::from_obj(&secret),
})?;
let certificate = x509_cert::Certificate::load_pem_chain(&certificate_data.0)
.with_context(|_| ReadChainSnafu {
secret: ObjectRef::from_obj(&secret),
})?
.remove(0);
debug!("retrieving private key data from secret via key {key_certificate:?}");
let private_key_data =
data.get(key_private_key)
.with_context(|| NoPrivateKeyDataSnafu {
secret: ObjectRef::from_obj(&secret),
})?;
let private_key_data =
std::str::from_utf8(&private_key_data.0).context(DecodeUtf8StringSnafu)?;
let signing_key_pair =
S::from_pkcs8_pem(private_key_data).context(DeserializeKeyFromPemSnafu)?;
Ok(Self {
certificate_pair: CertificatePair {
key_pair: signing_key_pair,
certificate,
},
})
}
/// Create a [`CertificateAuthority`] from a Kubernetes [`SecretReference`].
#[instrument(
name = "create_certificate_authority_from_k8s_secret_ref",
skip(client)
)]
pub async fn from_secret_ref(
secret_ref: &SecretReference,
key_certificate: &str,
key_private_key: &str,
client: Client,
) -> Result<Self, SecretError<S::Error>> {
let secret_api = Api::namespaced(client, &secret_ref.namespace);
let secret = secret_api
.get(&secret_ref.name)
.await
.with_context(|_| GetSecretSnafu {
secret_ref: secret_ref.to_owned(),
})?;
Self::from_secret(secret, key_certificate, key_private_key)
}
/// Returns the ca certificate.
pub fn ca_cert(&self) -> &Certificate {
&self.certificate_pair.certificate
}
}
impl CertificateAuthority<rsa::SigningKey> {
/// High-level function to create a new CA using a RSA key pair.
#[instrument(name = "create_certificate_authority_with_rsa")]
pub fn new_rsa() -> Result<Self> {
Self::new(rsa::SigningKey::new().context(GenerateRsaSigningKeySnafu)?)
}
}
impl CertificateAuthority<ecdsa::SigningKey> {
/// High-level function to create a new CA using a ECDSA key pair.
#[instrument(name = "create_certificate_authority_with_ecdsa")]
pub fn new_ecdsa() -> Result<Self> {
Self::new(ecdsa::SigningKey::new().context(GenerateEcdsaSigningKeySnafu)?)
}
}
fn format_leaf_certificate_subject(name: &str, scope: &str) -> Result<Name> {
let subject = format!("CN={name} Certificate for {scope}");
Name::from_str(&subject).context(ParseSubjectSnafu { subject })
}
#[cfg(test)]
mod tests {
use const_oid::ObjectIdentifier;
use super::*;
const TEST_CERT_LIFETIME: Duration = Duration::from_hours_unchecked(1);
const TEST_SAN: &str = "product-0.product.default.svc.cluster.local";
#[tokio::test]
async fn rsa_key_generation() {
let mut ca = CertificateAuthority::new_rsa().expect("must be able to create RSA-based CA");
let cert = ca
.generate_rsa_leaf_certificate("Product", "pod", [TEST_SAN], TEST_CERT_LIFETIME)
.expect(
"Must be able to generate an RSA certificate. Perhaps there was an RNG failure",
);
assert_cert_attributes(cert.certificate());
}
#[tokio::test]
async fn ecdsa_key_generation() {
let mut ca =
CertificateAuthority::new_ecdsa().expect("must be able to create ECDSA-based CA");
let cert = ca
.generate_ecdsa_leaf_certificate("Product", "pod", [TEST_SAN], TEST_CERT_LIFETIME)
.expect(
"Must be able to generate an ECDSA certificate. Perhaps there was an RNG failure",
);
assert_cert_attributes(cert.certificate());
}
fn assert_cert_attributes(cert: &Certificate) {
let cert = &cert.tbs_certificate;
let expected_subject = Name::from_str("CN=Product Certificate for pod")
.expect("constant subject must be valid");
// Test subject
assert_eq!(cert.subject, expected_subject);
// Test SAN extension is present
let extensions = cert.extensions.as_ref().expect("cert must have extensions");
assert!(
extensions
.iter()
.any(|ext| ext.extn_id == ObjectIdentifier::new_unwrap("2.5.29.17"))
);
// Test lifetime
let not_before = cert.validity.not_before.to_system_time();
let not_after = cert.validity.not_after.to_system_time();
assert_eq!(
not_after
.duration_since(not_before)
.expect("notBefore must be before notAfter"),
*TEST_CERT_LIFETIME
);
}
}