Skip to content

Expose SAN/IAN otherName entries via the FFI GeneralName API - #5903

Open
moritzschmitt wants to merge 4 commits into
randombit:masterfrom
moritzschmitt:ffi-othername-access
Open

moritzschmitt wants to merge 4 commits into
randombit:masterfrom
moritzschmitt:ffi-othername-access

Conversation

@moritzschmitt

@moritzschmitt moritzschmitt commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5885, implementing the direction agreed on the issue: otherName entries are surfaced through the existing botan_x509_general_name_t enumeration (Option A), with the type-id returned as the FFI's first-class OID object rather than as a string.

C++ library

Botan::GeneralName now retains the otherName payload (type-id OID plus the raw inner value) in its NameVariant, reusing AlternativeName::OtherNameValue. A factory GeneralName::other_name() and an accessor GeneralName::other_name_value() are added, GeneralName::decode_from() retains type-id and value for otherName name constraints (using the same structure checks as AlternativeName::decode_from()), and GeneralName::encode_into() can now encode otherNames, so the corpus re-encode test covers them.

One consequence: a malformed otherName in a NameConstraints extension (value not wrapped in the EXPLICIT [0] tag, missing type-id or value, trailing content) now fails to decode, where previously it was retained as an opaque entry without a payload. The extension then becomes an Unknown_Extension with failed_to_decode set, and path validation reports EXTENSION_ENCODING_ERROR (matching how a malformed otherName in a SubjectAlternativeName already behaves).

FFI

botan_x509_cert_subject_alternative_names(), botan_x509_cert_issuer_alternative_names() and the name constraint enumerators now yield otherName entries (appended after the previously supported types, so existing indices are unchanged), and botan_x509_general_name_get_type() returns BOTAN_X509_OTHER_NAME instead of erroring. For an otherName:

  • the new botan_x509_general_name_other_name_type_id() returns the type-id as a new OID object (to be freed with botan_oid_destroy), following botan_pubkey_oid(); callers can compare it via botan_oid_equal() against e.g. botan_oid_from_string("Microsoft UPN").
  • botan_x509_general_name_view_binary_value() views the raw BER of the value, i.e. the complete TLV inside the EXPLICIT [0] tag (exactly what AlternativeName::OtherNameValue::value() holds).
  • botan_x509_general_name_view_string_value() succeeds for values of the string types that AlternativeName::decode_from() also decodes into the deprecated other_names() set — UTF8String, IA5String, PrintableString, VisibleString and NumericString with content valid for the type — which directly covers the Microsoft UPN. Since the string view hands out a NUL-terminated C string, values with an embedded NUL are additionally rejected; both view functions document this. Anything else returns BOTAN_FFI_ERROR_INVALID_OBJECT_STATE.

The FFI changes are additive (new API version 20260901 API stamps are bumped at release time, exports tagged 3.14). As discussed in the issue, existing callers will observe additional SAN/IAN entries, get_type() succeeds for otherName name constraints where it previously errored, and SmtpUTF8Mailbox entries (RFC 9598) appear as otherNames. The stale "no way to access OTHER_NAME" comment in ffi.h and the "deprecated" sentence in ffi.rst are removed.

Python binding

botan3.py previously had no bindings for the GeneralName API at all; this adds X509GeneralName/X509GeneralNameType plus the X509Cert accessors for subject/issuer alternative names and permitted/excluded name constraints, along with documentation and tests.

Test data

src/tests/data/x509/othername/ contains generated certificates covering the msUPN case, every accepted and rejected string type, non-string values, multiple otherNames beside other name types, an IssuerAlternativeName otherName, otherName name constraints, and an empty as well as a 3000-byte value. The generator script is committed (src/scripts/dev_tools/gen_othername_testdata.sh); the README documents each certificate and the expected type-ids and inner values, which were read off openssl asn1parse output independently of the implementation. String values that are invalid for their tag cannot be produced with OpenSSL, so one certificate is built in the test itself via the C++ API. The existing otherName certificates under name_constraints/ are reused to pin the behavior for SmtpUTF8Mailbox, SRVName, permanentIdentifier and SIM entries.

@arckoor
arckoor self-requested a review September 2, 2026 18:06

@arckoor arckoor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rough review only, and only really of the parts I'm comfortable with, i.e. FFI and Python.

I'm not sure exposing X509GeneralName in the python binding directly is a good call, I've been wanting to add something like this for a while, see this branch. I'm not convinced what's done there is the way to go either (since highly inefficient if you do want everything), but perhaps parts are still of interest

Comment thread doc/api_ref/ffi.rst Outdated
============== ===================
FFI Version Supported Starting
============== ===================
20260901 3.14.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

3.14 would be 20261103, usually it's the date of the actual release. May be better to leave out completely and let it be handled in a separate PR, but as long as no one else opens a PR also looking to do this at the same time should probaly be fine :p

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair point, I wasn't sure about this either, but 3.14 was too tempting a version to leave unclaimed. If you'd rather keep the bump out of this PR, I'll drop it. Otherwise it can be adjusted to the actual release date the way the 3.11 stamp was.

Comment thread src/lib/ffi/ffi_cert.cpp
Comment on lines +832 to +834
if(Botan::any_null_pointers(oid)) {
return BOTAN_FFI_ERROR_NULL_POINTER;
}

@arckoor arckoor Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This before the #ifdef please, so behaviour re nullptrs doesn't change based on how the lib is compiled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I went back and forth on this one and ended up following the immediate neighbours: the GeneralName functions already in the file (botan_x509_general_name_get_type, the *_alternative_names / *_name_constraints enumerators and their _count variants) all do their argument checks inside the #if, and so does botan_x509_cert_serial_number, which has the same shape (out-pointer for a new object plus a handle). The file has both styles: not_before, get_fingerprint, get_public_key and the ext_* getters check before the #if, the newer X.509 additions check inside.

The other thing that held me back is that moving the oid check alone doesn't quite give config-independent behaviour: the handle is validated by BOTAN_FFI_VISIT, which sits inside the #if in every function of this file, so (&oid, nullptr) still returns NOT_IMPLEMENTED in a build without X.509 either way. Since no botan_x509_general_name_t can exist in such a build at all, NOT_IMPLEMENTED for any argument combination seemed a reasonable answer to me.

That said, it's a two-line change and I don't feel strongly about it. If you'd rather have the out-pointer checked first, I'll move it, and would suggest doing the same for the rest of the GeneralName block in a small follow-up so the API stays uniform. Happy to do either.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah I agree, it's kind of a mess. The ones that use the FFI_VISIT macro will always perform differently, but for FFI functions I've written in the past that require an explicit nullptr check, I've usually followed the (mostly self-imposed) "check before ifdef" rule. It's mostly a matter of taste, not a big deal either way :)

@moritzschmitt

Copy link
Copy Markdown
Contributor Author

I'm not sure exposing X509GeneralName in the python binding directly is a good call, I've been wanting to add something like this for a while, see this branch. I'm not convinced what's done there is the way to go either (since highly inefficient if you do want everything), but perhaps parts are still of interest

Thanks for the pointer, I had a look and tried the branch against the test certs. I think the two approaches want different things: subject_alt_name(type) -> list[str] is handy for the "give me the DNS names" case, but it can't carry a directoryName (the string view raises there, and the handle isn't freed on that path), an IP constraint with its mask, or an otherName, which needs the type-id next to the value. That's why I suggest to go with a small wrapper object. The value type depends on the name type, and the object lets the caller pick the right accessor, the same way X509CRL.revoked() returns X509CRLEntry objects rather than tuples. It's also the cheaper option for "everything": one pass over the SAN instead of one per type (11 vs 50 enumerations on the 11-entry test cert).

If a strings-only convenience for the common case would be useful, I'm happy to add one on top of the object list, in a single pass.

@arckoor

arckoor commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

The thought about the handle potentially not being freed is a good point, I hadn't considered (yet anyway). What might also work is wrapping the list[str] in an entirely new class, and having that class contain a .filter(type: X509GeneralNameType) -> list[str]. Not saying to do that right now, just food for thought

@moritzschmitt

Copy link
Copy Markdown
Contributor Author

Agreed, a filter helper on top of the object list would be a natural follow-up. The one thing to decide there is what filter(DIRECTORY_NAME) returns, since that type has no string form.

Previously GeneralName::decode_from only recorded that a name constraint
is an otherName and dropped its type-id and value, so such constraints
could neither be inspected nor re-encoded.

Add a NameVariant alternative holding the type-id OID and the raw BER
encoding of the inner value (reusing AlternativeName::OtherNameValue),
the factory GeneralName::other_name and the accessor
GeneralName::other_name_value. decode_from now parses the OtherName
structure the same way AlternativeName::decode_from does and rejects
malformed input, and encode_into emits it again, so otherName
constraints round trip byte identically.

GH randombit#5885
Certificates with otherName entries in the subject and issuer alternative
names and in the name constraints, covering the Microsoft UPN, every
string type that AlternativeName decodes into a string, string types it
does not, non-string values, and empty and very long values. The README
next to the data lists the contents and the expected values.

Generated with OpenSSL 3.6.1 by running
src/scripts/dev_tools/gen_othername_testdata.sh from the repository root.

GH randombit#5885
The subject and issuer alternative name enumerators of the FFI omitted
otherName entries, and botan_x509_general_name_get_type refused
otherName handles, which were reachable only via the name constraint
accessors. The Microsoft UPN used for certificate based authentication
against Active Directory is such an entry, so it was not accessible to
FFI users at all.

Enumerate otherNames after the other name types, let
botan_x509_general_name_get_type report BOTAN_X509_OTHER_NAME, view the
raw BER encoding of the value via
botan_x509_general_name_view_binary_value and, for UTF8String and UTF-8
subset string values, the string via
botan_x509_general_name_view_string_value. The new function
botan_x509_general_name_other_name_type_id returns the type-id as an
OID object.

Bump the FFI API version and drop the outdated remarks that otherNames
cannot be accessed via the FFI.

GH randombit#5885
Bind the botan_x509_general_name_t functions, including the enumerators
for subject/issuer alternative names and name constraints that the
binding never covered, as X509GeneralName and X509GeneralNameType, and
add the corresponding accessors to X509Cert. The module now targets the
FFI API version introduced with the otherName support in 3.14.0.

GH randombit#5885
@moritzschmitt

Copy link
Copy Markdown
Contributor Author

@reneme Since you wrote the GeneralName FFI that this PR extends, could you take a look? The design (otherName entries in the existing enumeration, the type-id as an OID object) was agreed with randombit on #5885.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FFI: SAN otherName entries (e.g. Microsoft UPN) are not accessible

2 participants