NO-JIRA: Add ECDSA P-256 support for certificates and CAs#2116
NO-JIRA: Add ECDSA P-256 support for certificates and CAs#2116fabiendupont wants to merge 1 commit intoopenshift:masterfrom
Conversation
|
@fabiendupont: This pull request explicitly references no jira issue. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
8dd03c3 to
724c6f6
Compare
Adds support for generating ECDSA P-256 certificates by specifying the key algorithm via the new service annotation: service.beta.openshift.io/serving-cert-key-algorithm: ecdsa When the annotation is not specified or set to "rsa", the operator generates RSA certificates for full backwards compatibility. This enables services to opt into modern elliptic curve cryptography, which provides equivalent security to 3072-bit RSA with significantly smaller keys (~87% smaller) and better performance. Implementation: - Added ServingCertKeyAlgorithmAnnotation constant in api.go - Modified MakeServingCert() to check annotation and select algorithm - Uses library-go's new MakeServerCertWithAlgorithm() API - Validates annotation values (rsa, ecdsa) with helpful error messages - Case-insensitive algorithm matching Testing: - Added TestECDSACertificateGeneration with 5 test cases - Verifies RSA (default and explicit), ECDSA, and invalid inputs - All existing tests pass with no regressions Depends on: openshift/library-go#2116 Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Fabien Dupont <fdupont@redhat.com>
724c6f6 to
b5ed9f4
Compare
| } | ||
|
|
||
| // MakeServerCertWithAlgorithm creates a server certificate with the specified key algorithm | ||
| func (ca *CA) MakeServerCertWithAlgorithm(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { |
There was a problem hiding this comment.
Hmm, I might be wrong, but it looks like MakeServerCert and MakeServerCertWithAlgorithm are very similar. There are two differences. The first is the key algorithm, and the second is that MakeServerCertWithAlgorithm sets the signature algorithm.
I’m wondering if it would make sense to create a private common function for both. I think something like the following could work:
func (ca *CA) MakeServerCertWithAlgorithm(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) {
sigFn := func(template *x509.Certificate) error {
template.SignatureAlgorithm = signatureAlgorithmForKey(ca.Config.Key)
return nil
}
fns = append([]CertificateExtensionFunc{sigFn}, fns...)
return ca.makeServerCertWithAlgorithm(hostnames, lifetime, algorithm, fns...)
}
and
func (ca *CA) MakeServerCert(hostnames sets.Set[string], lifetime time.Duration, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) {
return ca.makeServerCertWithAlgorithm(hostnames, lifetime, AlgorithmRSA, fns...)
}
pkg/crypto/crypto.go
Outdated
| case AlgorithmRSA: | ||
| return newKeyPairWithHash() | ||
| default: | ||
| return newKeyPairWithHash() // Default to RSA for backwards compatibility |
There was a problem hiding this comment.
Since this is a private method I think we could return an error for unsupported algorithms.
| } | ||
|
|
||
| // MakeServerCertForDurationWithAlgorithm creates a server certificate with specified duration and algorithm | ||
| func (ca *CA) MakeServerCertForDurationWithAlgorithm(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { |
There was a problem hiding this comment.
am I right that the differences between MakeServerCertForDuration and MakeServerCertForDurationWithAlgorithm mirror those between MakeServerCert and MakeServerCertWithAlgorithm (key algorithm and signature algorithm)? If so, could we refactor them in the same way?
There was a problem hiding this comment.
Good feedback. Makes sense.
b5ed9f4 to
854a902
Compare
|
@p0lyn0mial, I extended the changes to add the ECDSA CA support as well, so we can have a full ECDSA chain. |
3e1f176 to
62ba714
Compare
62ba714 to
3a9f301
Compare
| template := &x509.Certificate{ | ||
| Subject: subject, | ||
|
|
||
| SignatureAlgorithm: x509.SHA256WithRSA, |
There was a problem hiding this comment.
OK. I think that the SignatureAlgorithm will be set by x509.CreateCertificate and our unit tests assert this. Is that correct ?
There was a problem hiding this comment.
Yes, that's correct. When SignatureAlgorithm is zero-valued in the template, x509.CreateCertificate infers it from the signing key — SHA256WithRSA for RSA keys, ECDSAWithSHA256 for ECDSA P-256 keys. Our tests assert the resulting SignatureAlgorithm on the generated certs for all combinations (RSA CA + RSA leaf, RSA CA + ECDSA leaf, ECDSA CA + RSA leaf, ECDSA CA + ECDSA leaf).
| case AlgorithmRSA: | ||
| return newKeyPairWithHash() | ||
| default: | ||
| return nil, nil, nil, fmt.Errorf("unsupported key algorithm: %d", algo) |
There was a problem hiding this comment.
It looks like the error message will take the form of unsupported key algorithm: 2, which isn't meaningful to someone debugging. Is there a way to make it more descriptive ?
There was a problem hiding this comment.
Good point. This default branch can only be reached if a new KeyAlgorithm constant is added to the const block (e.g. AlgorithmEd25519) without adding a corresponding case in this switch.
Since newKeyPairWithAlgorithm is private and the callers always pass one of the defined constants, a developer hitting this error would know they forgot to wire up their new algorithm.
I added a comment to clarify that, and kept %d so the numeric value is visible for debugging.
3a9f301 to
212d47c
Compare
| return makeSelfSignedCAConfigForSubjectAndDuration(subject, time.Now, caLifetime, AlgorithmRSA) | ||
| } | ||
|
|
||
| func MakeSelfSignedCAConfigForDurationWithAlgorithm(name string, caLifetime time.Duration, algorithm KeyAlgorithm) (*TLSCertificateConfig, error) { |
There was a problem hiding this comment.
I might be wrong but
MakeSelfSignedCAConfigForDurationWithAlgorithm & MakeCAConfigForDurationWithAlgorithm use newSigningCertificateTemplateForDuration which sets
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
I found RFC 5480 where Section 3 says:
If the keyUsage extension is present in a Certification Authority
(CA) certificate that indicates id-ecPublicKey in
SubjectPublicKeyInfo, then any combination of the following values
MAY be present:
digitalSignature;
nonRepudiation;
keyAgreement;
keyCertSign; and
cRLSign.
which tells me that x509.KeyUsageKeyEncipherment (in newSigningCertificateTemplateForDuration) should not be used for ECDSA only for RSA.
There was a problem hiding this comment.
Added a keyUsageForAlgorithm helper that returns:
- RSA: KeyEncipherment | DigitalSignature
- ECDSA: DigitalSignature only
Per RFC 5480 Section 3, KeyEncipherment is not valid for ECDSA keys. The CA, server, and intermediate cert templates now receive the algorithm and set KeyUsage accordingly. Tests assert the correct KeyUsage for both RSA and ECDSA certs.
The public client cert template functions (NewClientCertificateTemplate / NewClientCertificateTemplateForDuration) are unchanged since client cert creation currently always uses RSA.
| } | ||
|
|
||
| // MakeServerCertWithAlgorithm creates a server certificate with the specified key algorithm | ||
| func (ca *CA) MakeServerCertWithAlgorithm(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { |
There was a problem hiding this comment.
I think the issue described at https://github.com/openshift/library-go/pull/2116/changes#r2826380341 is also applicable to the server, client cert template.
212d47c to
4f7ae88
Compare
p0lyn0mial
left a comment
There was a problem hiding this comment.
Added a few more nits, overall lgtm.
Before merging, should we pull the changes to the service-ca-operator to make sure the backward compatibility path works?
Also, are we planning some e2e tests for the service-ca-operator to cover the new path?
pkg/crypto/crypto.go
Outdated
| // keyUsageForAlgorithm returns the appropriate KeyUsage for the given algorithm. | ||
| // RSA keys use KeyEncipherment (for RSA key transport in TLS) + DigitalSignature. | ||
| // ECDSA keys use only DigitalSignature per RFC 5480 Section 3. | ||
| func keyUsageForAlgorithm(algorithm KeyAlgorithm) x509.KeyUsage { |
There was a problem hiding this comment.
nit: could we rename this to baseKeyUsageForAlgorithm (or something similar) to indicate the returned value is a starting point that callers may need to extend?
There was a problem hiding this comment.
Done. Renamed to baseKeyUsageForAlgorithm to make it clear the returned value is a starting point that callers extend (e.g. | KeyUsageCertSign for CAs).
pkg/crypto/crypto_test.go
Outdated
| case *rsa.PrivateKey: | ||
| return x509.SHA256WithRSA | ||
| default: | ||
| return x509.SHA256WithRSA |
There was a problem hiding this comment.
why not to t.Fatal for unrecognized values?
There was a problem hiding this comment.
Done. signatureAlgorithmForKey now takes testing.TB and calls t.Fatalf on unrecognized key types instead of silently defaulting to RSA.
4f7ae88 to
7b058d5
Compare
pkg/crypto/crypto_test.go
Outdated
| return x509.SHA256WithRSA | ||
| default: | ||
| t.Fatalf("unrecognized private key type: %T", key) | ||
| return 0 |
There was a problem hiding this comment.
nit: this is unreachable code - i think this could be removed.
There was a problem hiding this comment.
Unfortunately Go requires it — t.Fatalf does not have a "never returns" type, so the compiler needs a return statement to satisfy the function signature. Added a // unreachable, but required by the compiler comment and moved the t.Fatalf + return outside the switch to make it clearer.
There was a problem hiding this comment.
mhm, ok, thanks for checking.
7b058d5 to
4ffdff6
Compare
Adds support for generating ECDSA P-256 certificates by specifying the key algorithm via the new service annotation: service.beta.openshift.io/serving-cert-key-algorithm: ecdsa When the annotation is not specified or set to "rsa", the operator generates RSA certificates for full backwards compatibility. This enables services to opt into modern elliptic curve cryptography, which provides equivalent security to 3072-bit RSA with significantly smaller keys (~87% smaller) and better performance. Implementation: - Added ServingCertKeyAlgorithmAnnotation constant in api.go - Modified MakeServingCert() to check annotation and select algorithm - Uses library-go's new MakeServerCertWithAlgorithm() API - Validates annotation values (rsa, ecdsa) with helpful error messages - Case-insensitive algorithm matching - Wired ECDSA CA support: caConfig now accepts a KeyAlgorithm field, initializeSigningSecret passes algorithm to CA creation - CA rotation supports both RSA and ECDSA keys: removed RSA-only type assertion, rotateSigningCA and createIntermediateCACert accept crypto.PrivateKey, algorithm is detected and preserved on rotation Testing: - Added TestECDSACertificateGeneration with 5 test cases - Verifies RSA (default and explicit), ECDSA, and invalid inputs - Added e2e tests: ECDSA cert provisioning, invalid algorithm rejection, and ECDSA cert regeneration after corruption - All existing tests pass with no regressions Depends on: openshift/library-go#2116 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Fabien Dupont <fdupont@redhat.com>
|
/lgtm /hold @fabiendupont thanks for the pr and all your work :) |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: fabiendupont, p0lyn0mial The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
benluddy
left a comment
There was a problem hiding this comment.
This looks reasonable. I spoke with @sanchezl about potential overlap with openshift/enhancements#1882 and I don't think it would be harmful to grow a second way to do this in the future or have to do some refactoring.
pkg/crypto/crypto.go
Outdated
| authorityKeyId := ca.Config.Certs[0].SubjectKeyId | ||
| subjectKeyId := publicKeyHash | ||
| serverTemplate := newServerCertificateTemplateForDuration(pkix.Name{CommonName: sets.List(hostnames)[0]}, sets.List(hostnames), lifetime, time.Now, authorityKeyId, subjectKeyId) | ||
| serverTemplate := newServerCertificateTemplateForDuration(pkix.Name{CommonName: sets.List(hostnames)[0]}, sets.List(hostnames), lifetime, time.Now, authorityKeyId, subjectKeyId, algorithm) |
There was a problem hiding this comment.
The difference between MakeServerCert[WithAlgorithm] and MakeServerCertForDuration[WithAlgorithm] appears to come down to whether they call newServerCertificateTemplate or newServerCertificateTemplateForDuration, and those two have evolved in a strange way.
The first accepted the lifetime as number of days, applied a default, then converted it to a time.Duration before delegating to the ...ForDuration func. Last year, this was changed so that both accept time.Duration. The remaining differences are (1) the first form applies a default lifetime and (2) has a side effect of writing to stderr.
I think this means that MakeServerCert[WithAlgorithm] will apply a default when the provided lifetime is <= 0, whereas MakeServerCertForDuration[WithAlgorithm] will not. I doubt this difference is obvious to anyone who might be using this package. I agree with what Fabien has done in this PR (i.e., MakeServerCertWithAlgorithm is consistent with MakeServerCert, and MakeServerCertForDuration is consistent with MakeServerCertForDurationWithAlgorithm), but this feels like something we should improve later (cc @p0lyn0mial @sanchezl).
There was a problem hiding this comment.
sgtm, the distinction is almost invisible.
There was a problem hiding this comment.
Done. Consolidated the two paths:
- Removed
makeServerCertForDuration—MakeServerCertForDurationandMakeServerCertForDurationWithAlgorithmnow delegate tomakeServerCert. - Removed the
newServerCertificateTemplatelifetime-defaulting wrapper —newServerCertificateTemplateForDurationis now the solenewServerCertificateTemplate. - Updated
TestValidityPeriodOfServerCertificateto remove the test cases for0and negative lifetime that relied on the removed defaulting behavior.
pkg/crypto/crypto_test.go
Outdated
| @@ -151,7 +168,11 @@ func newSigningCertificateTemplate(subject pkix.Name, lifetime time.Duration, cu | |||
| warnAboutCertificateLifeTime(subject.CommonName, DefaultCACertificateLifetimeDuration) | |||
| } | |||
|
|
|||
| return newSigningCertificateTemplateForDuration(subject, lifetime, currentTime, nil, nil) | |||
| algo := AlgorithmRSA | |||
| if len(algorithm) > 0 { | |||
| algo = algorithm[0] | |||
| } | |||
| return newSigningCertificateTemplateForDuration(subject, lifetime, currentTime, nil, nil, algo) | |||
There was a problem hiding this comment.
Instead of making the function variadic with a default (and tossing all but the first KeyAlgorithm), can we make the KeyAlgorithm a normal parameter and update the callers to always provide a value?
There was a problem hiding this comment.
Done. newSigningCertificateTemplate now takes algorithm KeyAlgorithm as a required parameter. All callers updated to explicitly pass AlgorithmRSA (or tt.caAlgorithm for the mixed test).
pkg/crypto/crypto_test.go
Outdated
| // Verify public key matches private key | ||
| require.True(t, publicKey.X.Cmp(privateKey.PublicKey.X) == 0, "public key X should match") | ||
| require.True(t, publicKey.Y.Cmp(privateKey.PublicKey.Y) == 0, "public key Y should match") |
There was a problem hiding this comment.
Would https://pkg.go.dev/crypto/ecdsa#PublicKey.Equal be suitable for this test?
There was a problem hiding this comment.
Done. Replaced the manual X.Cmp/Y.Cmp checks with publicKey.Equal(&privateKey.PublicKey).
Adds support for generating ECDSA P-256 certificates by specifying the key algorithm via the new service annotation: service.beta.openshift.io/serving-cert-key-algorithm: ecdsa When the annotation is not specified or set to "rsa", the operator generates RSA certificates for full backwards compatibility. This enables services to opt into modern elliptic curve cryptography, which provides equivalent security to 3072-bit RSA with significantly smaller keys (~87% smaller) and better performance. Implementation: - Added ServingCertKeyAlgorithmAnnotation constant in api.go - Modified MakeServingCert() to check annotation and select algorithm - Uses library-go's new MakeServerCertWithAlgorithm() API - Validates annotation values (rsa, ecdsa) with helpful error messages - Case-insensitive algorithm matching - Wired ECDSA CA support: caConfig now accepts a KeyAlgorithm field, initializeSigningSecret passes algorithm to CA creation - CA rotation supports both RSA and ECDSA keys: removed RSA-only type assertion, rotateSigningCA and createIntermediateCACert accept crypto.PrivateKey, algorithm is detected and preserved on rotation Testing: - Added TestECDSACertificateGeneration with 5 test cases - Verifies RSA (default and explicit), ECDSA, and invalid inputs - Added e2e tests: ECDSA cert provisioning, invalid algorithm rejection, and ECDSA cert regeneration after corruption - All existing tests pass with no regressions Depends on: openshift/library-go#2116 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Fabien Dupont <fdupont@redhat.com>
|
|
||
| // newECDSAKeyPair generates a new P-256 ECDSA key pair | ||
| func newECDSAKeyPair() (*ecdsa.PublicKey, *ecdsa.PrivateKey, error) { | ||
| privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
There was a problem hiding this comment.
Not a review, just a note for the PKI feature team that will refactor this down the line openshift/enhancements#1882
Support RSA (with configurable key sizes: 2048, 3072, 4096) and ECDSA (with configurable curves: P-256, P-384, P-521) algorithms in the initial implementation
/cc @rh-roman @sanchezl We'll have to thread the key size here and for newKeyPairWithHash() for the RSA case.
library-go/pkg/crypto/crypto.go
Line 1005 in 5d9eb62
And through newKeyPairWithAlgorithm() as well.
Extends the crypto package to generate ECDSA P-256 key pairs and certificates in addition to existing RSA support, enabling OpenShift components to use modern elliptic curve cryptography. Adds: - KeyAlgorithm type for algorithm selection (RSA, ECDSA) - ECDSA P-256 key pair generation with proper SubjectKeyIdentifier hashing - MakeServerCertWithAlgorithm() and MakeServerCertForDurationWithAlgorithm() - MakeSelfSignedCAConfigForDurationWithAlgorithm() for ECDSA root CAs - MakeCAConfigForDurationWithAlgorithm() for ECDSA intermediate CAs Removes hardcoded SignatureAlgorithm from certificate templates, letting x509.CreateCertificate infer the correct algorithm from the signing key. This is backward compatible and enables correct cross-algorithm signing (e.g. RSA CA signing ECDSA leaf certs). All existing APIs remain unchanged, preserving backwards compatibility. New functionality is opt-in through *WithAlgorithm variants. Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Fabien Dupont <fdupont@redhat.com>
4ffdff6 to
125dfb4
Compare
|
New changes are detected. LGTM label has been removed. |
|
@fabiendupont: all tests passed! Full PR test history. Your PR dashboard. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. |
Extends the crypto package to generate ECDSA P-256 key pairs and certificates in addition to existing RSA support, enabling OpenShift components to use modern elliptic curve cryptography.
Changes
Key generation:
KeyAlgorithmtype for algorithm selection (AlgorithmRSA,AlgorithmECDSA)newKeyPairWithAlgorithm()for unified key generation dispatchServer certificates:
CA.MakeServerCertWithAlgorithm()andCA.MakeServerCertForDurationWithAlgorithm()CA certificates:
MakeSelfSignedCAConfigForDurationWithAlgorithm()for ECDSA root CAsMakeCAConfigForDurationWithAlgorithm()for ECDSA intermediate CAsTemplate cleanup:
SignatureAlgorithm: x509.SHA256WithRSAfrom certificate templatesx509.CreateCertificateinfers the correct algorithm from the signing key (SHA256WithRSA for RSA, ECDSAWithSHA256 for ECDSA P-256)All existing APIs remain unchanged — new functionality is opt-in through
*WithAlgorithmvariants.Test coverage
MakeServerCertForDurationWithAlgorithm(production code path fromcertrotation/target.go)