Skip to content

Use template for CA API (and configurable subject encoding)聽#2653

Open

Description

@Gabgobie

Hello!

  • Vote on this issue by adding a 馃憤 reaction
  • If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)

Issue details

I am working with a constrained CA that uses the UTF-8 encoding. The name constraints dictate that the subject must be under one of two allowed base DNs.

  • DC=org,DC=example
  • O=Example Org,OU=Branch Office

To be valid, my leafs must use a subject like

O=Example Org,OU=Branch Office,CN=Webservice + for example SAN www.example.org

or

O=Example Org,OU=Branch Office,CN=www.example.org

As discussed in #1917, step-ca can only issue certificates using the PrintableString encoding, meaning all certificate are invalid.

Firefox rejects them with SEC_ERROR_CERT_NOT_IN_NAME_SPACE

My preferred fix would be to make the encoding either globally or per field configurable.

An alternative solution I have considered is the use of the subject.ExtraNames which seem to be a workaround for more complex DNs but I don't know how they are correctly used in the template.

Why is this needed?

Step CA is currently unable to issue valid certificates in my environment.

Activity

  1. Gabgobie commented on Apr 17, 2026

    @Gabgobie
    Author

    Maybe this can be achieved per field using asn1enc but I think this would still not solve the re-encoding of the CN field.

    asn1Enc "utf8:foobar"

    Edit:

    Possibly by wrapping it in the marshall thing?

    (asn1Marshal (.Insecure.CR.Subject.CommonName) "utf8") or similar?

  2. Gabgobie commented on Apr 17, 2026

    @Gabgobie
    Author

    Edit: I didn't get it quite right, see the below comment.


    // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
    // x509 Certificate blocks.
    type ASN1DN struct {
    Country string `json:"country,omitempty"`
    Organization string `json:"organization,omitempty"`
    OrganizationalUnit string `json:"organizationalUnit,omitempty"`
    Locality string `json:"locality,omitempty"`
    Province string `json:"province,omitempty"`
    StreetAddress string `json:"streetAddress,omitempty"`
    SerialNumber string `json:"serialNumber,omitempty"`
    CommonName string `json:"commonName,omitempty"`
    }
    // AuthConfig represents the configuration options for the authority. An
    // underlaying registration authority can also be configured using the
    // cas.Options.
    type AuthConfig struct {
    *cas.Options
    AuthorityID string `json:"authorityId,omitempty"`
    DeploymentType string `json:"deploymentType,omitempty"`
    Provisioners provisioner.List `json:"provisioners,omitempty"`
    Admins []*linkedca.Admin `json:"-"`
    Template *ASN1DN `json:"template,omitempty"`
    Claims *provisioner.Claims `json:"claims,omitempty"`
    Policy *policy.Options `json:"policy,omitempty"`
    DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
    Backdate *provisioner.Duration `json:"backdate,omitempty"`
    EnableAdmin bool `json:"enableAdmin,omitempty"`
    DisableGetSSHHosts bool `json:"disableGetSSHHosts,omitempty"`
    }

    So if I got this right the "authority" level "template" is different from the provisioner level "template". At least that gives me a bit of an explanation why I haven't seen my template used at all. Step-CA is unable to issue a valid cert for itself and acme clients refuse to connect due to the invalid cert. Therefore I only tried connecting directly to step to check my certs.

    Now I still didn't get it to issue any DN values of my choice but if I am correct, the effort is futile anyways because the server certificate step issues for itself is also required to contain an explicit policy, which, given the different template struct, is impossible.

  3. Gabgobie commented on Apr 18, 2026

    @Gabgobie
    Author

    At the risk of derailing the issue, I was digging around and this function should be the one issuing the API certificate.

    // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
    func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
    fatal := func(err error) (*tls.Certificate, error) {
    return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
    }
    // Generate default key.
    priv, err := keyutil.GenerateDefaultKey()
    if err != nil {
    return fatal(err)
    }
    signer, ok := priv.(crypto.Signer)
    if !ok {
    return fatal(errors.New("private key is not a crypto.Signer"))
    }
    // prepare the sans: IPv6 DNS hostname representations are converted to their IP representation
    sans := make([]string, len(a.config.DNSNames))
    for i, san := range a.config.DNSNames {
    if strings.HasPrefix(san, "[") && strings.HasSuffix(san, "]") {
    if ip := net.ParseIP(san[1 : len(san)-1]); ip != nil {
    san = ip.String()
    }
    }
    sans[i] = san
    }
    // Create initial certificate request.
    cr, err := x509util.CreateCertificateRequest(a.config.CommonName, sans, signer)
    if err != nil {
    return fatal(err)
    }
    // Generate certificate template directly from the certificate request.
    template, err := x509util.NewCertificate(cr)
    if err != nil {
    return fatal(err)
    }
    // Get x509 certificate template, set validity and sign it.
    now := time.Now()
    certTpl := template.GetCertificate()
    certTpl.NotBefore = now.Add(-1 * time.Minute)
    certTpl.NotAfter = now.Add(24 * time.Hour)
    // Policy and constraints require this fields to be set. At this moment they
    // are only present in the extra extension.
    certTpl.DNSNames = cr.DNSNames
    certTpl.IPAddresses = cr.IPAddresses
    certTpl.EmailAddresses = cr.EmailAddresses
    certTpl.URIs = cr.URIs
    // Fail if name constraints do not allow the server names.
    if err := a.constraintsEngine.ValidateCertificate(certTpl); err != nil {
    return fatal(err)
    }
    // Set the cert lifetime as follows:
    // i) If the CA is not a StepCAS RA use 24h, else
    // ii) if the CA is a StepCAS RA, leave the lifetime empty and
    // let the provisioner of the CA decide the lifetime of the RA cert.
    var lifetime time.Duration
    if casapi.TypeOf(a.x509CAService) != casapi.StepCAS {
    lifetime = 24 * time.Hour
    }
    resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
    Template: certTpl,
    CSR: cr,
    Lifetime: lifetime,
    Backdate: 1 * time.Minute,
    IsCAServerCert: true,
    })
    if err != nil {
    return fatal(err)
    }
    // Generate PEM blocks to create tls.Certificate
    pemBlocks := pem.EncodeToMemory(&pem.Block{
    Type: "CERTIFICATE",
    Bytes: resp.Certificate.Raw,
    })
    for _, crt := range resp.CertificateChain {
    pemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{
    Type: "CERTIFICATE",
    Bytes: crt.Raw,
    })...)
    }
    keyPEM, err := pemutil.Serialize(priv)
    if err != nil {
    return fatal(err)
    }
    tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM))
    if err != nil {
    return fatal(err)
    }
    // Set leaf certificate
    tlsCrt.Leaf = resp.Certificate
    return &tlsCrt, nil
    }

    If I got this right, the CSR is only using Config.CommonName and entirely ignores Config.AuthorityConfig.Template

  4. changed the title [-]Configurable subject encoding[/-] [+]Configurable subject (and encoding)[/+] on Apr 18, 2026
  5. changed the title [-]Configurable subject (and encoding)[/-] [+]Use template for CA API (and configurable subject encoding)[/+] on Apr 18, 2026
  6. Gabgobie commented on Apr 23, 2026

    @Gabgobie
    Author

    After opening the issue for the initial suspect of subject encoding I discovered that the issue is a bit deeper as is apparent from my other comments. If it is of any value, I can try to reword the opening comment. ^^

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementneeds triageWaiting for discussion / prioritization by team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions