Skip to content

Add support for subject DN components in MacKMS certificate URIs - #1073

Merged
hslatman merged 5 commits into
masterfrom
feat/mackms-subject-search
Aug 14, 2026
Merged

Add support for subject DN components in MacKMS certificate URIs#1073
hslatman merged 5 commits into
masterfrom
feat/mackms-subject-search

Conversation

@joshdrake

@joshdrake joshdrake commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Adds subject distinguished name components — cn, o, ou, l, st, c — to the mackms certificate URI vocabulary, so certificates can be selected by subject in LoadCertificate, LoadCertificateChain, and DeleteCertificate:

mackms:cn=My+Cert
mackms:cn=My+Cert;o=Smallstep;ou=Engineering;ou=Platform
mackms:label=test@example.com;ou=Engineering
mackms:serial=2c2739...;o=Smallstep

Constraint: subject matching happens in Go — we have to enumerate certificates

The Apple keychain cannot do this filtering for us:

  • There are no per-RDN attributes — nothing like "issuer CN" or "subject OU" exists at the keychain layer.
  • kSecMatchSubjectContains is a substring match over the whole subject and only works on legacy file-based keychains, not the data protection keychain.
  • kSecAttrSubject/kSecAttrIssuer hold the entire name as an Apple-normalized (partially uppercased) DER blob. They are read-only, system-derived, exact-match columns, and there is no public API to produce the normalized bytes from user-supplied strings — the SecCertificateCopyNormalized*Sequence APIs all require a SecCertificateRef in hand. The existing comment in LoadCertificateChain already rejects cert.RawIssuer/RawSubject matching for this reason.

So when a URI contains subject components, mackms fetches the candidates (kSecMatchLimitAll), parses every candidate with x509.ParseCertificate, and matches the components against cert.Subject in Go. label and/or serial in the same URI still narrow the keychain query natively first; a pure-DN query like mackms:ou=Engineering enumerates and parses every certificate in the keychain. That's fine at keychain scale (tens to a few hundred items), but combining with label is recommended, and the docs say so. This mirrors what capi does on Windows, where cn is matched in Go while iterating a native issuer-search cursor.

Semantics

  • All given components must match (logical AND) using exact, case-sensitive comparison.
  • cn is single-valued; o/ou/l/st/c can be repeated, and every given value must be present in the certificate subject (subset match on multi-valued RDNs).
  • When multiple certificates match, the existing selection applies (most recent already-valid NotBefore).
  • LoadCertificateChain: components select the leaf only; intermediates are still located via the authority key identifier.
  • DeleteCertificate: with subject components present, the certificate is located first (same rules as LoadCertificate) and then deleted by serial number plus, when available, subject key identifier. Still deletes at most one certificate.

Behavior change

Subject component attributes in certificate URIs were previously ignored; now they filter. For example, mackms:label=x;cn=y used to load by label alone and can now return not found, and mackms:cn=y used to fail with "label or serial is required" and now performs a lookup.

Testing

  • New Test_parseCertURI (first unit coverage for this function), Test_certAttributes_hasSubjectQuery, and Test_certAttributes_matchesSubject — pure, no keychain access.
  • New integration tests TestMacKMS_LoadCertificate_bySubject and TestMacKMS_DeleteCertificate_bySubject (including a no-SKID self-signed cert for the serial-only delete path), plus subject cases in TestMacKMS_LoadCertificateChain.
  • ⚠️ CI does not build or run mackms (darwin+cgo only; CI is Linux). Verified here: gofmt, go build ./..., go vet, and the kms/uri/kms/platform/kms/apiv1 tests. The package still needs go test ./kms/mackms/... on a Mac before this merges — keeping as draft until that run happens.

Possible follow-ups (out of scope)

  • Go-side issuer component filters (issuer-cn, …) for disambiguating same-subject certs from different CAs.
  • Chain building via kSecAttrSubject == SecCertificateCopyNormalizedIssuerSequence(child) (macOS 10.12.4+) as an alternative to the SKID walk — with a cert in hand the framework computes the normalized bytes, so no reimplementation is needed; kSecAttrIssuer is an indexed primary-key column, making this the one place issuer-based keychain-side narrowing is legitimate.
  • Because pure-DN queries enumerate the whole keychain, one unparseable foreign certificate fails the load loop (pre-existing behavior, now more exposed). Could skip unparseable certs when a subject query is present.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P3Nxd2zBbULcRAG6HrzQgR

@CLAassistant

CLAassistant commented Jul 16, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ hslatman
❌ joshdrake
You have signed the CLA already but the status is still pending? Let us recheck it.

@hslatman hslatman self-assigned this Aug 12, 2026

@hslatman hslatman 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.

Functionally looks OK. Successfully tested it with a small test program to locate certs in my login keychain using cn= and ou= (independently).

Comment thread kms/mackms/mackms.go
Comment on lines +734 to +740
if u.hasSubjectQuery() {
// The keychain cannot match individual subject components. Find the
// matching certificate first, using the same selection rules as
// LoadCertificate, and delete it by its serial number and, when
// available, its subject key identifier.
cert, err := loadCertificate(u, nil)
if err != nil {

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.

If the whole keychain is searched, a single malformed certificate (as determined by the Go parser) can result in loadCertificate failing, as it returns early in the loop when failing to parse the cert. The error itself will likely also confuse the reader, as it may not even be clear which cert fails to parse.

Either change the early return to continue, skipping the malformed (certs), or provide an additional option in loadCertificate to explicitly ask to continue instead of the early return.

Comment thread kms/mackms/mackms.go Outdated
useDataProtectionKeychain: isDataProtectionKeychain(keychain, useDataProtectionKeychain),
keychain: keychain,
}, nil
commonName: u.Get("cn"),

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.

To make it even more robust, cn can be processed similarly to the other components, and then pick the first one. Even better would be to return an error if multiple cns are provided.

Comment thread kms/mackms/mackms.go
Comment on lines +701 to +711
// request name. It deletes at most one certificate.
//
// Valid names (URIs) are:
// - mackms:label=test@example.com
// - mackms:serial=2c273934eda8454d2595a94497e2395a
// - mackms:label=test@example.com;serial=2c273934eda8454d2595a94497e2395a
// - mackms:cn=My+Cert;ou=Engineering
//
// When subject components ("cn", "o", "ou", "l", "st", "c") are present, the
// certificate is first located using the same selection rules as
// [MacKMS.LoadCertificate], and then deleted by its serial number and, when

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.

If I understand this correctly, a subject search could match multiple certificates, and then only a single one would get deleted? Although the comment mentions it, it could be somewhat surprising.

Given the function name, I think the behavior is OK, and I think it can still go in. I don't have a great alternative atm.

Comment thread kms/mackms/mackms.go
Comment on lines +743 to +749
serialNumber = cert.SerialNumber
if len(cert.SubjectKeyId) > 0 {
cfSubjectKeyID, err := cf.NewData(cert.SubjectKeyId)
if err != nil {
return fmt.Errorf("mackms DeleteCertificate failed: %w", err)
}
defer cfSubjectKeyID.Release()

@hslatman hslatman Aug 14, 2026

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.

Probably minor, as I believe the SKI will be filled, but if it's not, then the actual delete will be by serial only. While generally random, the serial is only unique in combination with the issuer.

I believe this behavior existed before, so it's not something new that breaks if this gets merged.

joshdrake and others added 4 commits August 14, 2026 21:26
Support selecting certificates in the Apple Keychain by subject
distinguished name components using the new "cn", "o", "ou", "l", "st",
and "c" URI attributes, e.g. "mackms:cn=My+Cert;ou=Engineering".

The keychain cannot match individual subject components, and it stores
normalized (partially uppercased) subject values, so the components are
used to filter the parsed certificates after retrieving the candidates
from the keychain. All the given components must match using exact,
case-sensitive comparisons, and the multi-valued components can be
repeated, requiring every value to be present in the subject.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3Nxd2zBbULcRAG6HrzQgR
When subject components ("cn", "o", "ou", "l", "st", "c") are present in
the URI, find the matching certificate first, using the same selection
rules as LoadCertificate, and delete it by its serial number and, when
available, its subject key identifier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3Nxd2zBbULcRAG6HrzQgR
When seaching based on subject components, ignore errors caused by
parsing certificates from the Apple keychain.
@hslatman
hslatman force-pushed the feat/mackms-subject-search branch from 10f57aa to ddd2124 Compare August 14, 2026 19:26
@hslatman hslatman changed the title mackms: support subject DN components in certificate URIs Add support for subject DN components in MacKMS certificate URIs Aug 14, 2026
@hslatman
hslatman marked this pull request as ready for review August 14, 2026 19:31
@hslatman
hslatman requested review from a team and maraino August 14, 2026 19:31
@hslatman

Copy link
Copy Markdown
Member

While testing this I found that certificates from the login as well as system keychains are considered in the subject match. I'm not sure if that's explicitly known behavior, and I don't think it currently breaks use cases, as we've been relying on e.g. lookup by serial, but I thought I'd make a note of it.

There's currently a switch in which we decide whether to use the data protection keychain vs. "the regular one", which is also chosen when it says login. That could be changed/extended to have traditional (or something like that) point to all regular keychains (possibly in a keychain-type arg?), and then have keychain point to a named keychain (e.g. login, system, custom named), so that the filter can become more granular using kSecMatchSearchList.

@maraino maraino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The changes look ok, but should we be deleting the oldest certificate instead of the new one.

@hslatman
hslatman force-pushed the feat/mackms-subject-search branch from 48f3416 to d6585c7 Compare August 14, 2026 21:35
@hslatman
hslatman requested a review from maraino August 14, 2026 21:38

@maraino maraino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

@hslatman
hslatman merged commit 77837ea into master Aug 14, 2026
11 of 12 checks passed
@hslatman
hslatman deleted the feat/mackms-subject-search branch August 14, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants