Skip to content

Support subject, literalSubject, and usages for CA and self-signed certificates. - #813

Open
MartinWeindel wants to merge 12 commits into
masterfrom
enh/subject-and-usages
Open

Support subject, literalSubject, and usages for CA and self-signed certificates.#813
MartinWeindel wants to merge 12 commits into
masterfrom
enh/subject-and-usages

Conversation

@MartinWeindel

@MartinWeindel MartinWeindel commented Aug 25, 2026

Copy link
Copy Markdown
Member

How to categorize this PR?

/kind enhancement

What this PR does / why we need it:
Support subject, literalSubject, and usages for CA and self-signed certificates.

  • subjectFromInput builds the pkix.Name from either the structured subject or a parsed literalSubject LDAP DN.
  • For literalSubject, the parsed RDN sequence is DER-encoded and set as RawSubject on the CSR/certificate template, preserving exact attribute order and non-structured attribute types (e.g. DC) that Go would otherwise reorder or drop.
  • The common name (structured or extracted from the literal subject) is promoted to a DNS SAN for leaf certificates, but not for CA certificates.
  • Configurable key usages are applied, always including cert-sign for CA/self-signed certs.

Notes

  • No change to DN ordering behavior was required — literalSubject order is preserved end-to-end. DER stores RDNs in the reverse of the RFC 4514 string form by spec, so openssl asn1parse shows root-first; openssl x509 -nameopt rfc2253 renders back the exact input.

Tests

  • Unit tests cover structured/literal subject population, attribute-order and non-structured-attribute preservation through CSR creation and issuance, CN→SAN promotion rules, and key-usage defaulting.

Which issue(s) this PR fixes:
Fixes #794

Special notes for your reviewer:

Release note:

Support `subject`, `literalSubject`, and `usages` for CA and self-signed certificates.

@gardener-prow

gardener-prow Bot commented Aug 25, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign marc1404 for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gardener-prow gardener-prow Bot added kind/enhancement Enhancement, improvement, extension size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Aug 25, 2026
MartinWeindel and others added 12 commits August 25, 2026 14:58
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
…objects

Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
…tificates

Extend the CA and self-signed issuance paths to honor the structured
Subject, LiteralSubject and Usages fields of the Certificate spec:

- subjectFromInput builds the pkix.Name from either the parsed literal
  subject (LDAP DN) or CommonName plus the structured Subject attributes,
  falling back to the CA-derived attributes otherwise.
- buildKeyUsages maps api.KeyUsage values to x509 key/extended-key usages,
  always including KeyCertSign for CA certificates and defaulting to
  server auth when no usages are requested.
- NewSelfSignedCertInPEMFormat and the CA/self-signed constructors now
  return the resulting common name so the reconciler can reconstruct the
  original spec and derive the status common name from a literalSubject.
- Promote the common name (including one extracted from a literalSubject)
  to a DNS SAN for leaf certificates.

Add unit and integration test coverage for these paths.

Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Go 1.25+ deprecates direct access to the raw ecdsa.PublicKey coordinate
fields (X/Y). Use the Equal method implemented by all standard library
public key types instead, which compares the underlying values without
touching the deprecated fields and safely returns false for mismatched
key types. The (bool, error) contract and error-on-unknown-type behavior
are preserved; ed25519 keys are now handled as well.

Signed-off-by: Martin Weindel <martin.weindel@sap.com>
ToKeyUsages split the usages annotation on "," without trimming, so a
natural value like "server auth, client auth" produced " client auth",
which failed the known-usage check and was silently dropped. Trim each
token before the lookup so surrounding whitespace is tolerated.

Signed-off-by: Martin Weindel <martin.weindel@sap.com>
Signed-off-by: Martin Weindel <martin.weindel@sap.com>
…roller

The certman2 source controller (common.CertInput / CreateSpec) handled
issuer, preferredChain, privateKey and renewBefore annotations but not
the literal-subject and usages annotations that the legacy source
controller already syncs onto the Certificate spec. As a result, a
Gateway/Service/Ingress annotated with cert.gardener.cloud/literal-subject
or cert.gardener.cloud/usages produced a Certificate that silently
dropped these fields.

Add the AnnotLiteralSubject and AnnotUsages constants, extend CertInput
with LiteralSubject and Usages, read them in augmentFromCommonAnnotations,
and map them into the CertificateSpec in CreateSpec, mirroring the legacy
controller. Extend the ingress and service reconciler tests accordingly.

Signed-off-by: Martin Weindel <martin.weindel@sap.com>
@MartinWeindel
MartinWeindel force-pushed the enh/subject-and-usages branch from 80d399f to 62f3761 Compare August 25, 2026 13:01
@MartinWeindel

Copy link
Copy Markdown
Member Author

/cla

Comment on lines +1094 to +1147
if spec.LiteralSubject != nil {
h.Write([]byte("literalSubject"))
h.Write([]byte{0})
h.Write([]byte(*spec.LiteralSubject))
h.Write([]byte{0})
}
if spec.Subject != nil {
s := spec.Subject
for _, v := range s.Organizations {
h.Write([]byte("O="))
h.Write([]byte(v))
h.Write([]byte{0})
}
for _, v := range s.Countries {
h.Write([]byte("C="))
h.Write([]byte(v))
h.Write([]byte{0})
}
for _, v := range s.OrganizationalUnits {
h.Write([]byte("OU="))
h.Write([]byte(v))
h.Write([]byte{0})
}
for _, v := range s.Localities {
h.Write([]byte("L="))
h.Write([]byte(v))
h.Write([]byte{0})
}
for _, v := range s.Provinces {
h.Write([]byte("ST="))
h.Write([]byte(v))
h.Write([]byte{0})
}
for _, v := range s.StreetAddresses {
h.Write([]byte("STREET="))
h.Write([]byte(v))
h.Write([]byte{0})
}
for _, v := range s.PostalCodes {
h.Write([]byte("PC="))
h.Write([]byte(v))
h.Write([]byte{0})
}
if s.SerialNumber != "" {
h.Write([]byte("SN="))
h.Write([]byte(s.SerialNumber))
h.Write([]byte{0})
}
}
for _, u := range spec.Usages {
h.Write([]byte("usage="))
h.Write([]byte(u))
h.Write([]byte{0})
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the length of this function at this point, we could add a little helper function that takes an optional prefix, writes the payload, and adds the 0 trailer. What do you think?

Comment on lines +53 to +57
for usage := range strings.SplitSeq(value, ",") {
if set.Has(string(usage)) {
usages = append(usages, api.KeyUsage(usage))
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, it doesn't allow for extraneous spaces in the key usages. Do we want to allow that and implicitly trim the spaces before appending the key usage?

email: some.user@mydomain.com
precheckNameservers:
- 8.8.8.8
- 8.8.4.4 No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there was a trailing newline here before

@marc1404 marc1404 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't verified whether it actually happens, but it might be that the current tests don't capture this case: I think there could be a hash mismatch scenario with a CSR-based Certificate that would cause it to be reprovisioned constantly.

The hash is using the CommonName from the spec:

if spec.CommonName != nil {
h.Write([]byte(*spec.CommonName))
h.Write([]byte{0})
}
(when using a CSR only this would be nil)

When reconstructing the spec, it populates CommonName from the LiteralSubject now. This should only happen when CSR is nil:

if cert.Spec.LiteralSubject == nil {
spec.CommonName = result.CommonName
}

certInput.LiteralSubject = value
}
if value := annotations[AnnotUsages]; value != "" {
certInput.Usages = shared.ToKeyUsages(value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: shared.ToKeyUsages silently drops unrecognized values (whereas this is highlighted when having them in the resource spec


var usages []api.KeyUsage
if value, ok := resources.GetAnnotation(objData, AnnotUsages); ok && value != "" {
usages = shared.ToKeyUsages(value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: shared.ToKeyUsages silently drops unrecognized values (whereas this is highlighted when having them in the resource spec

Comment on lines +488 to +501
if ptr.Deref(spec.LiteralSubject, "") != info.LiteralSubject {
if info.LiteralSubject != "" {
s := info.LiteralSubject
spec.LiteralSubject = new(s)
} else {
spec.LiteralSubject = nil
}
mod.Modify(true)
}

if !reflect.DeepEqual(spec.Usages, info.Usages) {
spec.Usages = info.Usages
mod.Modify(true)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: LiteralSubject and Usages are correctly set in the updateEntry path. However, in createEntryFor, they are not set when building the spec

func (r *sourceReconciler) createEntryFor(logger logger.LogContext, obj resources.Object, info CertInfo, feedback CertFeedback) error {
cert := &api.Certificate{}
cert.GenerateName = strings.ToLower(r.nameprefix + obj.GetName() + "-" + obj.GroupKind().Kind + "-")
resources.SetAnnotation(cert, AnnotForwardOwnerRefs, "true")
if r.targetclass != "" {
resources.SetAnnotation(cert, AnnotClass, r.targetclass)
}
if len(info.Domains) > 0 {
if len(info.Domains[0]) <= 64 {
cert.Spec.CommonName = &info.Domains[0]
cert.Spec.DNSNames = info.Domains[1:]
} else {
cert.Spec.CommonName = nil
cert.Spec.DNSNames = info.Domains
}
}
if info.IssuerName != nil {
parts := strings.SplitN(*info.IssuerName, "/", 2)
if len(parts) == 2 {
cert.Spec.IssuerRef = &api.IssuerRef{Namespace: parts[0], Name: parts[1]}
} else {
cert.Spec.IssuerRef = &api.IssuerRef{Name: *info.IssuerName}
}
}
cert.Spec.SecretRef = &core.SecretReference{
Name: info.SecretName.Name,
Namespace: info.SecretName.Namespace,
}
if r.namespace == "" {
cert.Namespace = obj.GetNamespace()
} else {
cert.Namespace = r.namespace
}
if info.FollowCNAME {
cert.Spec.FollowCNAME = &info.FollowCNAME
}
cert.Spec.SecretLabels = info.SecretLabels
if info.PreferredChain != "" {
cert.Spec.PreferredChain = &info.PreferredChain
}
cert.Spec.PrivateKey = createPrivateKey(info.PrivateKeyAlgorithm, info.PrivateKeySize, info.PrivateKeyEncoding)
// Set renewBefore (validation will happen in certificate reconciler)
if info.RenewBefore != nil {
cert.Spec.RenewBefore = info.RenewBefore
}
for key, value := range info.Annotations {
resources.SetAnnotation(cert, key, value)
}
e, _ := r.SlaveResoures()[0].Wrap(cert)
err := r.Slaves().CreateSlave(obj, e)
if err != nil {
if feedback != nil {
feedback.Failed(&info, err)
}
return err
}
obj.Eventf(core.EventTypeNormal, "reconcile", "created certificate object %s", e.ObjectName())
logger.Infof("created certificate object %s", e.ObjectName())
if feedback != nil {
feedback.Pending(&info, "")
}
return nil
}

// +optional
IsCA *bool `json:"isCA,omitempty"`
// Usages defines the requested key usages and extended key usages.
// If unset, defaults to `digital signature` and `key encipherment`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: The comment is slightly off; key encipherment is only added for RSA keys:

if isRSA {
ku |= RSAKeyUsage
}

What about:

Suggested change
// If unset, defaults to `digital signature` and `key encipherment`.
// If unset, defaults to `digital signature` (and `key encipherment` for RSA keys).

Comment on lines +218 to +220
if src.LiteralSubject != "" {
spec.LiteralSubject = &src.LiteralSubject
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier in the function CommonName is set, but there's no exclusivity check here to use either CommonName or LiteralSubject

if len(src.Domains) > 0 {
if len(src.Domains[0]) <= 64 {
spec.CommonName = &src.Domains[0]
spec.DNSNames = normalizeArray(src.Domains[1:])
} else {
spec.CommonName = nil
spec.DNSNames = src.Domains
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement Enhancement, improvement, extension size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Need native support for SUBJECT and USAGE in Gardener Cert Manager

2 participants