-
-
Notifications
You must be signed in to change notification settings - Fork 15
feat!(stackable-certs): Use builder pattern and support SAN entries #1044
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just commenting on a few things. Mostly thoughts.
/// .build() | ||
/// .build_ca() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels a little awkward to me. I'm not (yet) sure what the two build methods do differently?
It looks like the build()
method still returns a builder, and build_ca()
self-signs and returns a CertificateAuthority.
Point is, I don't find it intuitive and I think it should be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worked it out below, and recommended renaming the struct.
/// .expect("failed to build CA"); | ||
/// ``` | ||
#[derive(Builder)] | ||
pub struct CertificateAuthorityBuilder<'a, SKP> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be:
pub struct CertificateAuthorityBuilder<'a, SKP> | |
pub struct CertificateAuthority<'a, SKP> |
So when we call build()
, we get back a CA and not a CA Builder.
Note, the Builder macro would otherwise generate CertificateAuthorityBuilderBuilder
(ie: it generates the Builder, not us).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To illustrate further:
CertificateAuthority::builder() -> CertificateAuthorityBuilder
CertificateAuthorityBuilder::build() ->
a validCertificateAuthority
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, but we already have a struct: CertificateAuthority<SK>
at
pub struct CertificateAuthority<SK> |
in my mind, that one should be the one that derives Builder. Otherwise we need to revise what each struct is, because the current CertificateAuthorityBuilder
is not a builder (per previous message above).
crates/stackable-certs/src/ca/mod.rs
Outdated
let subject = Name::from_str(SDP_ROOT_CA_SUBJECT).context(ParseSubjectSnafu { | ||
subject: SDP_ROOT_CA_SUBJECT, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: While we are at it, I think this should use expect
instead. This subject name must always pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I tend to disagree here. My argumentation would be that stackable-certs doesn't know where the subject is coming from. It could be CN=<pod-name>
, but maybe also a certificate we issue for users (user-provided subject?), so they can connect to e.g. mTLS Kafka.
Better safe than sorry I'd say
crates/stackable-certs/src/ca/mod.rs
Outdated
#[snafu(display("The subject alternative DNS name \"{dns_name}\" is not a Ia5String"))] | ||
SaDnsNameNotAIa5String { | ||
dns_name: String, | ||
source: x509_cert::der::Error, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Rename the variant and adjust error message.
#[snafu(display("The subject alternative DNS name \"{dns_name}\" is not a Ia5String"))] | |
SaDnsNameNotAIa5String { | |
dns_name: String, | |
source: x509_cert::der::Error, | |
}, | |
#[snafu(display("failed to parse subject alternative name {subject_alternative_name:?} as a Ia5 string"))] | |
ParseSubjectAlternativeName { | |
subject_alternative_name: String, | |
source: x509_cert::der::Error, | |
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to
#[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,
},
in 8f86a93
/// Serial number of the generated certificate. | ||
/// | ||
/// If not specified a random serial will be generated. | ||
serial_number: Option<u64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: This is an internal detail we shouldn't allow being customized by the user of this library. I'm sure there is something like
/// Serial number of the generated certificate. | |
/// | |
/// If not specified a random serial will be generated. | |
serial_number: Option<u64>, | |
/// Serial number of the generated certificate. | |
#[builder(skip)] | |
serial_number: Option<u64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. On that topic, if we want to have proper CAs... then serial should increment.
So, if we restore a CA (eg: on startup, read from a secret to get the CA keypair), we also need information for the certs that have been issued (not by reading available signed certs, since they might be gone already). We basically need an atomic counter.
I'm not 100% certain of the downside if we just always issue the same version though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied the builder to mimic the current api, in which users can specify the serial:
pub fn new_with(signing_key_pair: S, serial_number: u64, validity: Duration) -> Result<Self> { |
secret-op generates random serials for cas and certs (https://github.com/stackabletech/secret-operator/blob/00909cfb92e61539a4baab9190235cd33b33a61b/rust/operator-binary/src/backend/tls/ca.rs#L258 and https://github.com/stackabletech/secret-operator/blob/00909cfb92e61539a4baab9190235cd33b33a61b/rust/operator-binary/src/backend/tls/mod.rs#L306), this seems to be fine.
I will update the PR to not let users choose the serial
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Cryptographic keypair used to sign leaf certificates. | ||
/// | ||
/// If not specified a random keypair will be generated. | ||
signing_key_pair: Option<SKP>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I would like to see the same high-level functions to create a RSA or ECDSA based CA.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in fd19a03
/// 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_SECONDS`]) | ||
/// | ||
/// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: Compared to these docs, the current docs lack in depth and technical explanations. I've spent a significant amount of time on these and would like to see them carried over to the new implementation as complete as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried carrying over everything that is still relevant: 1322779
/// Serial number of the generated certificate. | ||
/// | ||
/// If not specified a random serial will be generated. | ||
serial_number: Option<u64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: Again, I would like this to be an internal details not exposed to the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as #1044 (comment)
/// Mandatorily sign the certificate using the provided [`CertificateAuthority`]. | ||
signed_by: &'a CertificateAuthority<KP>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: This should be part of the .build()
method on the CertificateBuilder
.
let ca_validity = self.signed_by.ca_cert().tbs_certificate.validity; | ||
let ca_not_after = ca_validity.not_after.to_system_time(); | ||
let cert_not_after = validity.not_after.to_system_time(); | ||
if ca_not_after < cert_not_after { | ||
warn!( | ||
ca.validity = ?ca_validity, | ||
cert.validity = ?validity, | ||
ca.not_after = ?ca_not_after, | ||
cert.not_after = ?cert_not_after, | ||
subject = ?subject, | ||
"The lifetime of certificate authority is shorted than the lifetime of the generated certificate", | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: Thank you for adding this.
note: This should be a hard error instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Optional list of subject alternative names (SAN) DNS entries, | ||
/// that are added to the certificate. | ||
#[builder(default)] | ||
subject_alterative_dns_names: &'a [&'a str], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I would just go with
/// Optional list of subject alternative names (SAN) DNS entries, | |
/// that are added to the certificate. | |
#[builder(default)] | |
subject_alterative_dns_names: &'a [&'a str], | |
/// Optional list of subject alternative names (SAN) DNS entries, | |
/// that are added to the certificate. | |
#[builder(default)] | |
subject_alterative_names: &'a [&'a str], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, there is no value in carrying this as &str
. They need to be converted into owned values below anyway. The API should reflect that to not hide (possibly) unexpected cloning/allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to allow for more things, like IP's, email addresses, etc... (just so we avoid premature restrictions)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Secret-op already does IP SANs, so yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I misread, I though @nightkr requested to support IPs... I added support for that in 8f86a93 🙈
But as this is a builder I prefer to have a function to add DNs names and one to add IP addresses instead of passing a list of enums (which we first need to introduce).
Regarding the allocation, I see the point that this allocates. On the other hand I prefer the simple api, so that not every caller needs to deal with der::asn1::ia5_string::allocation::Ia5String
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map(|dns_name| { | ||
Ok(GeneralName::DnsName(Ia5String::new(dns_name).context( | ||
SaDnsNameNotAIa5StringSnafu { | ||
dns_name: dns_name.to_string(), | ||
}, | ||
)?)) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: Please flatten this a little bit. I'm not a huge fan of these nested code snippets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The best I could come up was changing
let san_dns = self.subject_alterative_dns_names.iter().map(|dns_name| {
Ok(GeneralName::DnsName(
Ia5String::new(dns_name).with_context(|_| ParseSubjectAlternativeDnsNameSnafu {
subject_alternative_dns_name: dns_name.to_string(),
})?,
))
});
to
let san_dns = self.subject_alterative_dns_names.iter().map(
|dns_name| -> Result<_, CreateCertificateError<KP::Error>> {
Ia5String::new(dns_name)
.with_context(|_| ParseSubjectAlternativeDnsNameSnafu {
subject_alternative_dns_name: dns_name.to_string(),
})
.map(GeneralName::DnsName)
},
);
Not sure if that really improves the situation
Description
Needed for CRD conversions, as the apiserver needs to trust our cert
Current example code
Definition of Done Checklist