Skip to content

step certificate lint: a --profile flag, and retiring the zlint/zcrypto forks #1723

Open

Description

@edmundlod

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'm looking into packaging step-ca and step for Debian. Debian is unhappy
with vendored forks of libraries it already ships, so I spent some time on
smallstep/zlint and smallstep/zcrypto (both untouched for years) to
work out what they are for and whether they are still needed.

As far as I can tell:

  • zlint its only functional change is one line, from 2018: ED25519SIG
    added to lint_signature_algorithm_not_supported, so Ed25519 certs don't
    fail.
    My reading: step is mostly used for internal PKI, and Ed25519 is
    fine there.
  • zcrypto carries Ed25519/X25519 support and the step_provisioner
    extension.

Two of those three reasons have since evaporated: zmap/zcrypto has had
Ed25519 and X25519 for a while, and it now parses the provisioner extension
too (zmap/zcrypto#545, merged 2026-10-21 — I sent it
while looking into this). That leaves the zlint line.

A different way to do it

A possible solution, which I have implemented and tested locally (link below),
is a new flag:

    step certificate lint --profile {private,webpki}
  • private, the default, applies only the lint families that make sense for
    an internal PKI: everything except the CA/Browser Forum and root-store
    families
  • webpki applies everything, including the CA/Browser Forum and root-store
    families, for certificates meant to be publicly trusted

Behind the scenes

  1. zlint v3 (more on that below) tags every lint with a source (RFC5280,
    CABF_BR, CABF_EV, Mozilla, Apple, ETSI_ESI, Community, …), and
  2. LintCertificateEx takes a filtered lint.Registry.

Improvements The smallstep-patched lint is a CABF Baseline Requirements
lint, so relaxing it makes step certificate lint report BR compliance
for a certificate that isn't BR-compliant, including when someone is
deliberately checking a public certificate!

Filtering via --profile means that an internal cert will pass (because CABF
lints weren't applied). As a result, the CABF signature-algorithm lint can
stay correct.

I tested this on a self-signed Ed25519 CA certificate:

today               231 lints   2 errors
--profile=private   150 lints   0 errors
--profile=webpki    338 lints   4 errors (incl. the Ed25519 ones)

Ed25519 still passes by default, so nobody suddenly starts seeing failures.
The output does change though: the default profile reports 150 lints rather
than 231, so anything parsing this will see fewer entries.

As a bonus, the two errors reported today (smallstep forks)

    e_ca_key_usage_missing
    e_root_ca_key_usage_present

also go away under the default (private) profile. They are BR-only too, and
don't apply to a private CA.

Implementation - A few changes needed

The catch is that the flag can't be built on top of the current
smallstep/zlint. Filtering lints by source is a zlint v3 feature, and the
fork was created before v3. smallstep/zlint has no lint.Registry,
no FilterOptions and no LintCertificateEx.
Its ResultSet simply runs every registered lint, with no way to select a
subset. So, the flag means moving to zmap/zlint/v3.

That pulls zcrypto along with it. zlint v3's LintCertificate takes a
zmap/zcrypto/x509.Certificate, so lint.go has to parse with zmap's
zcrypto rather than smallstep's. inspect.go is the only other file using
that type, and keeping it on the fork would mean compiling two near-identical
x509 packages into the same binary. My take: it would be best if it gets
removed as well.

Result: My suggestion would then be that both smallstep forks go.
This is, of course, what I was after (from the Debian packaging perspective).
But, as far as I can tell (and I might be missing something!) it actually
improves things anyway, now that we are a couple of years after the smallstep
forks were created:

  • Ed25519 and X25519 are in zmap/zcrypto already
  • The provisioner extension is too, because of the PR mentioned above
  • The Ed25519 lint behaviour is what --profile=private restores, but now
    more correctly?

I found two things that smallstep/zcrypto has, that upstream doesn't:

  1. root_js.go, a wasm build stub (not sure what uses it?), and
  2. extended_key_usage_gen.go, a code generator, with a hardcoded URL
    pointed at smallstep/constants instead of zmap/constants

I found the following while implementing this:

  • Go 1.27. zcrypto increased the required go version to 1.27.0 on
    2026-10-15 (7e1570b, PQC support in tls), so importing zcrypto requires
    at least that.

  • The provisioner JSON keys change. I followed zcrypto's snake_case
    rather than copying smallstep's:

    before  {"credentialID":"cred-1","env":"prod","name":"acme","type":"ACME"}
    after   {"type":"ACME","name":"acme","credential_id":"cred-1",
             "key_value_pairs":{"env":"prod"}}
    

    Nesting the pairs stops a (theoretical) metadata key called type
    overwriting the real one, but it is still a breaking change for anyone
    parsing this
    . I didn't realise this difference until after the PR had been
    merged (I followed a previous addition to their store to see how it needs to
    be done, and used that as an example to build my PR on). If you prefer it,
    I am sure that the PR can be amended to follow the smallstep key names.

Check it out

The branch is here if you want a look before deciding whether it is worth a
PR:

master...edmundlod:smallstep-cli:lint-profile

The code changes are rather small and few. The high line count comes from
go.sum mostly.

go build ./... and go test ./command/certificate/ both pass. I also
checked that step certificate inspect --format json still renders
step_provisioner as a named object, against a certificate I generated
carrying that OID, and that an invalid --profile value exits 1 with a
sensible message.

If this all looks fine to you, I would be very happy to open a proper PR. I
just thought I would first run it by you.

If you think this is not at all what you want, please let me know as well.
That is still useful to know. Happy to make any changes you want as well.

Why is this needed?

  1. For step users. step certificate lint currently applies CA/Browser
    Forum rules to every certificate, including the internal ones step-ca
    issues. That produces errors that don't apply (e_ca_key_usage_missing,
    e_root_ca_key_usage_present above), and in the other direction it
    reports BR compliance for certificates that aren't BR-compliant.
    A profile fixes both ends, and the default keeps today's behaviour
    for Ed25519.

  2. For the project. It retires two forks that haven't had a functional
    change since 2018, in favour of an actively maintained upstream.
    smallstep/cli is the only smallstep repo that depends on them.
    certificates, crypto and certinfo have no zcrypto or zlint in their go.mod.

  3. For packagers. Debian doesn't like shipping vendored forks of libraries
    already in the archive, so today step cannot easily be packaged there.

Activity

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