Repository navigation
step certificate lint: a --profile flag, and retiring the zlint/zcrypto forks #1723
Description
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 tolint_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
familieswebpkiapplies everything, including the CA/Browser Forum and root-store
families, for certificates meant to be publicly trusted
Behind the scenes
- zlint v3 (more on that below) tags every lint with a source (
RFC5280,
CABF_BR,CABF_EV,Mozilla,Apple,ETSI_ESI,Community, …), and LintCertificateExtakes a filteredlint.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/zcryptoalready - The provisioner extension is too, because of the PR mentioned above
- The Ed25519 lint behaviour is what
--profile=privaterestores, but now
more correctly?
I found two things that smallstep/zcrypto has, that upstream doesn't:
root_js.go, a wasm build stub (not sure what uses it?), andextended_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
goversion 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?
-
For step users.
step certificate lintcurrently 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_presentabove), 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. -
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. -
For packagers. Debian doesn't like shipping vendored forks of libraries
already in the archive, so today step cannot easily be packaged there.
Activity