Skip to content

X.509 Name support & invalid output w/ multiple RDNs #3203

@thanatos

Description

@thanatos

Describe the bug:

Ideally, one should be able to precisely control the generated certs, including the X.509 Subject. This is sort of there; in particular these are the relevant docs (for CertificateSpec); it has:

commonName: string
subject:
  organizations  []string | (Optional) Organizations to be used on the Certificate.
  countries  []string | (Optional) Countries to be used on the Certificate.
  organizationalUnits  []string | (Optional) Organizational Units to be used on the Certificate.
  localities  []string | (Optional) Cities to be used on the Certificate.
  provinces  []string | (Optional) State/Provinces to be used on the Certificate.
  streetAddresses  []string | (Optional) Street addresses to be used on the Certificate.
  postalCodes  []string | (Optional) Postal codes to be used on the Certificate.
  serialNumber  string | (Optional)

If we look at this through the lens of an X.509 certificate, this is peculiar. commonName is part of the subject, but isn't listed under subject. Subject itself is a mapping, so control over the actual ordering is lost, but a default would probably suffice for most purposes.

If we actually use this, say,

    organizationalUnits: [Databases, Postgres]

We get back the following certificate:

        Subject: C = US, …, OU = Databases + OU = Postgres, …, CN = postgres

The + here indicates that this is a "multi-valued" RDN (Subject is a sequence of RDNs, here separated by commas); LDAP wiki says,

In practice, Relative Distinguished Name containing multiple name-value pairs (called "Multi-Valued RDNs") are rare, but they can be useful at times when either there is no unique attribute in the entry or you want to ensure that the entry's Distinguished Name contains some useful identifying information.

The RDN sequence is supposed to indicate a location within a hierarchy, and particularly so with OU. A "multi-valued" RDN is a set, that is, A = foo + B = bar is a set (with no particular order) holding two things, A & B; it's used when the same entry in the hierarchy can be identified multiple ways. But we're not naming the same OU two different ways, we're trying to refer to two separate OUs in the hierarchy, that is, an OU named "Posgres" in the OU named "Databases", not a single thing.

This diagram from the X.501 standard is fairly illustrative of the intent:

A diagram of an RDN hierarchy

(The "OU = Sales, L = Ipswitch" level is a multi-valued RDN. What we are attempting to create would be two levels, each with just "OU = …".)

This particular multi-valued RDNs is also not valid:

The set that forms an RDN contains exactly one AttributeTypeAndValue for each attribute which contains distinguished values in the entry; that is, a given attribute type cannot appear twice in the same RDN.

(Here, the attribute type OU is appearing twice in the same RDN.)

Expected behaviour:

The most sensible thing, to me, is to just have the RDN sequence that the Subject is comprised of exposed to the user directly. Ignoring multi-valued RDN, that is a sequence of (type, value) pairs. Providing anything that isn't an RDN sequence leaves a user guessing as to what algorithm gets used to transform what is specified in a Certificate into the actual certificate.

(I understand having a more user-friendly layer, and an more raw layer. But the current interface doesn't really provide either.)

Steps to reproduce the bug:

Use the Secret and Issuer from the CA docs in the sandbox NS.
Call this cert.yaml:

apiVersion: cert-manager.io/v1beta1
kind: Certificate
metadata:
  name: pg.client.postgres
  namespace: sandbox
spec:
  subject:
    countries: [US]
    organizations: [Widget Co]
    organizationalUnits: [Databases, Postgres]
  commonName: postgres
  duration: 720h  # 30 d
  renewBefore: 336h  # 14 d
  secretName: pg.client.postgres
  issuerRef:
    kind: Issuer
    name: ca-issuer

Apply the Secret & Issuer from the docs, then apply this Certificate:

kubectl apply -n sandbox -f cert.yaml

Grab the resulting subject:

» kubectl get secret -n sandbox pg.client.postgres -o 'jsonpath={ .data.tls\.crt }' | base64 -d | openssl x509 -noout -text | grep Subject:
        Subject: C = US, O = Widget Co, OU = Databases + OU = Postgres, CN = postgres

Note that it's a (malformed) multi-valued RDN, instead of two RDNs indicating one OU within another OU; what I was going for was C = US, O = Widget Co, OU = Databases, OU = Postgres, CN = postgres.

Anything else we need to know?

The root of this seems to be that cert-manager, quite reasonably, uses golang's pkix module. But that module states,

ToRDNSequence converts n into a single RDNSequence. The following attributes are encoded as multi-value RDNs: [most of the attributes on CertificateSpec.subject]

By this it seems to mean that it, for all those "helpful" attributes on Name which are lists, it will create a single RDN with all entries from the list represented as a multi-value RDN, which will always be invalid for n > 1. The module also states,

Note that Name is only an approximation of the X.509 structure. If an accurate representation is needed, asn1.Unmarshal the raw subject or issuer as an RDNSequence.

…this is somewhat of an understatement: pkix's Name is not capable of generating arbitrary RDN sequences, it doesn't guarantee that an RDN sequence read into it will round-trip, and it generates malformed RDN sequences given reasonable inputs. It would be great, I think, to take the advice in the docs here, and use RDNSequence directly.

(It would seem to me that putting >1 element into any of pkix.Name's members would result in a malformed RDN, and likely not what a user knowledgeable in X.50x intended. I filed a bug about this with Go, but their position is mostly what I expected: the behavior is regrettable, but unchangeable due to stability guarantees.)

If cert-manager represented an RDNSequence directly in YAML, pretty much 1:1, exactly as it is defined, it would look something like this, I think:

subject:
  - C: US
  - O: FooCorp
  - OU: Databases
  - OU: Postgres

It's still fairly terse, and all valid representations are representable. Even multi-valued RDNs are possible (e.g., the standard's example would be - {OU: Sales, L: Ipswitch})

To implement this, it would require changing the format of CertificateSpec, and taking the route pkix suggests of using RDNSequence directly.

Environment details:

  • Kubernetes version (e.g. v1.10.2): v1.16.8
  • Cloud-provider/provisioner (e.g. GKE, kops AWS, etc): N/A (but none, in my setup.)
  • cert-manager version (e.g. v0.4.0): v0.16.1
  • Install method (e.g. helm or static manifests): helm

/kind bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugCategorizes issue or PR as related to a bug.priority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to complete.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions