Chapter Orientation

Chapter 78 taught reading TLV; this chapter uses it on the most important DER document — the X.509 certificate. A certificate is a CA's endorsement of a public key: "this key belongs to the subject named X, vouched for by the issuer named Y during such-and-such a period". xrtX509Parse turns DER into an xx509cert view in one step: version, serial, issuer, subject, validity, public key, extensions, signature — all borrowing the original DER, zero-copy and zero-allocation. This chapter covers every field of the view, the Name's RDN cursor, extension traversal, and the "signature-algorithm OID → normalized parameters" translation layer; the parser's built-in consumer-side validation checklist (structure, canonical encoding, field order) is listed in full. Verification (whether the signature holds) is Chapter 80's business — this chapter is responsible only for reading accurately what the certificate says.

79.1引入

What is the first thing a TLS client does upon receiving the server certificate? Not verifying the signature — parsing. Your code must answer a string of questions: who issued this certificate (find the parent)? What is the subject called (identity matching)? When does it expire (expiry check)? What type is the public key (pick the verification backend)? Which extensions are present (find SAN, find revocation endpoints)? Each question maps to one or two view fields of xx509cert.

The parser is itself a defense line: a certificate with illegal formatting (wrong field order, length overruns, non-canonical encodings, duplicate extension OIDs) is rejected at Parse — it does not wait until signature verification to crash. RFC 5280's certificate syntax tree has dozens of nodes; XRT internalizes them all: what you get is a fully validated view, not bare TLV to grope through yourself.

79.2概念

The certificate's structure: a signature wrapping an assertion

1tbsCertificateversion/serial/signature algorithm/issuer/validity/subject/public key/extensions — the endorsed assertion
2signatureAlgorithmthe algorithm the issuer actually used (AlgorithmIdentifier)
3signatureValuethe signature over tbsCertificate's DER (BIT STRING)

The relation of the three parts is Chapter 80's core: signatureValue is the signature over the whole DER byte range of tbsCertificate — that original text is exactly what verification takes. xx509cert retains the full certificate and TBS DER (Raw/Tbs), both AlgorithmIdentifiers (inner and outer — required to agree; historically some certificates differed, which parsing rejects), and the signature contents with the unused-bit offset removed.

The full view family: what one certificate reads out

After xrtX509Parse(DER, 长度, &证书视图) succeeds, the available facts:

FieldContentsNotes
Versionv1/v2/v3 natural numberingField absent means v1 (enum value 1), never written as 0
SerialThe serial's complete content octetsBorrowed view; positive sign bit preserved, no magnitude conversion — the consumer side accepts zero/negative/overlong historical values
Issuer/SubjectThe issuer's/subject's complete DERName structure, next section
NotBefore/NotAfterValidity, already converted to xtimeClosed interval; xrtX509ValidAt(&cert, xrtNow()) judges expiry in one step
SPKIThe SubjectPublicKeyInfo's complete DERPublic-key algorithm + material, section after next
ExtensionsExtension sequence (optional, v3)Cursor traversal, later
SignatureSignature contents (BIT STRING minus bit offset)For Chapter 80's verification

Two contracts deserve their own memory: validity is a closed interval, and with legal parameters "expired" returns only false, not a runtime error (expiry is a normal business state, not a parse fault); Serial keeps its original bytes — RFC 5280 requires CAs to use positive numbers no longer than 20 bytes, but the consumer side must gracefully handle the zero and negative serials that exist in real root stores; issuer-side constraints are the business of CA-side tooling.

Name: the subject is not a string but an attribute sequence

The certificate's Issuer/Subject is not "one name" but an RDN (Relative Distinguished Name) sequence — attribute groups like C=CN, O=Example, CN=api.example.test. The xrtX509NameInit/Read cursor produces attributes one by one: OID (identifying this as country/organization/common name) + value view. Equality of two Names compares DER bytes (not strings) — in Chapter 81's chain verification, "child's Issuer == parent's Subject" is exact byte matching. Important boundary: modern identity judgment does not read CN (Common Name) — RFC 9525 recognizes only SubjectAltName (Chapter 80); CN is display-only.

Public key and signature algorithm: the OID translation layer

Algorithms in the certificate are all identified by OID (a number string like 1.2.840.113549.1.1.10). XRT does not make you maintain an OID table:

  • xrtX509PublicKey(&cert, &公钥视图): parses the SPKI — producing a Type enum (RSA, RSA-PSS, EC, Ed25519, Ed448, plus the x25519/x448 curve family, independent of the currently compiled backends) with algorithm parameters and the public-key material view.
  • xrtX509AlgorithmParse(DER 视图, &算法) + xrtX509SignatureParse(&算法, &规范签名): translates the AlgorithmIdentifier into a normalized parameter structure — signature type (e.g. RSA-PSS), content digest, MGF1 digest, salt length, trailer — which Chapter 80's verification entry consumes directly. Missing parameters are filled per RFC defaults; illegal combinations error at the parse layer.

Extensions: the certificate's plugin system

The v3 certificate's extension area is a key-value sequence: OID + critical bit + value DER. The parser checks duplicate OIDs and structural legality; traversal uses the extension cursor producing entries one by one. Common extensions: SAN (Subject Alternative Name — the real source of DNS/IP identity), BasicConstraints (is it a CA, path-depth cap — the core input of chain verification), KeyUsage (key-usage bitmap), AKI/SKI (issuer/subject key identifiers — chain lookup indices), CRL distribution points (the entrance of Chapter 81's revocation check). Each extension's value is its own DER structure — XRT provides dedicated parse entries for SAN, BasicConstraints, NameConstraints, and CRL-related extensions (docs/api/x509.md per section); this chapter's cheat sheet lists the index.

The parser's validation checklist: consumer-side strictness

Inputs Parse rejects: wrong field count and order, explicit default version, non-canonical serial INTEGER, empty Issuer, time encodings that are not real calendar dates, NotAfter earlier than NotBefore, broken SPKI structure, optional fields contradicting the version (a v1 certificate carrying v3 fields), illegal extension-body DER, duplicate extension OIDs, TBS and outer signature algorithms disagreeing, illegal signature BIT STRING, empty Subject without a non-empty critical SAN. This checklist is "consumer-side strictness": every legal variant existing in the real world (zero serials, overlong serials) is accepted, every structural ambiguity is rejected — parseable = structurally unambiguous; the remaining authenticity judgment goes to the verification layer.

79.3示例

First complete program: zero-allocation parsing and subject traversal

The following program comes from examples/x509/inspect/main.c, parsing a built-in minimal certificate and reading out the version, subject attributes, and public-key type:

examples/x509/inspect/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>




















 */


/* 演示零分配解析证书,并遍历其 Subject Name。 */
int main(void)
{
	static const uint8 Certificate[] = {
		0x30, 0x68, 0x30, 0x5C, 0x02, 0x01, 0x01, 0x30, 0x04, 0x06, 0x02, 0x2A,
		0x03, 0x30, 0x11, 0x31, 0x0F, 0x30, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x03,
		0x0C, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x30, 0x1E, 0x17, 0x0D,
		0x32, 0x35, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
		0x5A, 0x17, 0x0D, 0x32, 0x36, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30,
		0x30, 0x30, 0x30, 0x5A, 0x30, 0x12, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03,
		0x55, 0x04, 0x03, 0x0C, 0x07, 0x53, 0x75, 0x62, 0x6A, 0x65, 0x63, 0x74,
		0x30, 0x0A, 0x30, 0x04, 0x06, 0x02, 0x2A, 0x03, 0x03, 0x02, 0x00, 0x4B,
		0x30, 0x04, 0x06, 0x02, 0x2A, 0x03, 0x03, 0x02, 0x00, 0x53
	};
	xx509cert Cert;
	xx509namecursor Cursor;
	xx509nameattr Attribute;
	xx509pubkey PublicKey;

	if ( !xrtX509Parse(Certificate, sizeof(Certificate), &Cert) ||
		!xrtX509NameInit(Cert.Subject, &Cursor) ||
		(xrtX509NameRead(&Cursor, &Attribute) != X509_VALUE) ||
		!xrtX509PublicKey(&Cert, &PublicKey) ) {
		return 1;
	}
	printf(
		"version=%d subject=%.*s key-type=%d\n",
		(int)Cert.Version,
		(int)Attribute.Value.Size, (const char*)Attribute.Value.Data,
		(int)PublicKey.Type
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/x509/inspect/main.c -lws2_32 -liphlpapiversion=1 subject=Subject key-type=0

What just happened. (1) The input is a 104-byte hand-built minimal certificate (the array inlined directly) — xrtX509Parse passes all structural checks in one step and produces the view; no heap allocation, and every pointer inside Cert points into the array. (2) The xrtX509NameInit + Read cursor takes the Subject's first attribute — the value view Subject (this certificate's CN); RDN traversal is isomorphic to Chapter 78's DER cursor, only with attribute semantics translated. (3) xrtX509PublicKey gives Type=0 (X509_KEY_UNKNOWN — this toy certificate's public-key algorithm OID is outside the known set); real certificates yield enums like RSA/EC/ED25519. (4) Return right after printing — the view borrows the array, no free call needed; in the real scenario, swapped for "the DER buffer decoded from PEM", the view's lifetime binds to that buffer (see Pitfall 1).

Second complete program: signature-algorithm parameter translation

The second program comes from examples/x509/signature/main.c, reading the full RSA-PSS parameter set from a standalone AlgorithmIdentifier:

examples/x509/signature/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>





















 */


/* 演示从独立 AlgorithmIdentifier 读取完整 RSA-PSS 参数。 */
int main(void)
{
	static const uint8 AlgorithmDer[] = {
		0x30, 0x0D,
		0x06, 0x09, 0x2A, 0x86, 0x48, 0x86,
		0xF7, 0x0D, 0x01, 0x01, 0x0A,
		0x30, 0x00
	};
	xx509algorithm Algorithm;
	xx509signature Signature;

	if ( !xrtX509AlgorithmParse(
		(xbytesview) { AlgorithmDer, sizeof(AlgorithmDer) }, &Algorithm
	) || (xrtX509SignatureParse(
		&Algorithm, &Signature
	) != X509_VALUE) ) {
		return 1;
	}
	printf(
		"type=%d hash=%d mgf-hash=%d salt=%zu trailer=%u\n",
		(int)Signature.Type, (int)Signature.Hash,
		(int)Signature.MaskHash, Signature.SaltSize,
		(unsigned)Signature.Trailer
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/x509/signature/main.c -lws2_32 -liphlpapitype=2 hash=1 mgf-hash=1 salt=20 trailer=1

What just happened. (1) The 15-byte input: a SEQUENCE wrapping the OID 1.2.840.113549.1.1.10 (id-RSASSA-PSS) and an empty parameter SEQUENCE. (2) Two-step translation: AlgorithmParse recognizes the OID and produces the algorithm identifier; SignatureParse fills the parameters (defaults included) into the normalized structure — type=2 is the RSA-PSS enum, hash=1 the SHA-256 family, MGF1 same hash, salt length 20 bytes (RFC 4055 default), trailer 0xBC. (3) What the caller receives is parameters that directly drive Chapter 76's crypto module — no OID table lookups, no default rules to manage; that is the meaning of the "OID translation layer". In the certificate scenario these two steps are internalized by CertificateVerify (Chapter 80), but the standalone entries work equally for CRL, OCSP, and custom signature objects.

79.4契约

  • Zero-copy view: every field of xx509cert borrows the input DER; the input memory must stay valid until the views are done; no allocation whatsoever, hence no free calls.
  • Structure is validation: parse success = field order/encoding/consistency all legal (the consumer-side checklist); structural ambiguity is rejected at Parse, error domain xrt.x509.
  • Validity: xtime form, closed interval; with legal parameters, xrtX509ValidAt reports expired/not-yet-valid as mere false, not errors.
  • Serial semantics: the canonical INTEGER's original content preserved (sign bit included); CA-side constraints (positive, ≤20 bytes) are enforced separately by issuance/compliance tooling.
  • Name equality: compared by DER bytes (xrtX509NameEqual), not string equivalence; identity judgment never reads CN (Chapter 80's SAN rules).
  • Algorithm translation: AlgorithmIdentifier → normalized parameters completes at the parse layer; defaults filled per RFC; illegal combinations error.
  • Public-key type: xx509keytype is independent of the compiled verification backends — parsing needs no cryptographic algorithms; verification (Chapter 80) trims per need.
  • Extensions: duplicate OIDs and illegal structure rejected at parse; SAN/BasicConstraints/NameConstraints/CRL extensions have dedicated entries (docs/api/x509.md).

Common Mistakes

Pitfall 1: the view's lifetime follows the DER buffer:Pitfall 1: the view's lifetime follows the DER buffer

Symptoms: parsing succeeds, fields are all read, then a later access crashes or reads garbage — typically in a wrapper that "decodes PEM inside a function and returns the certificate view".

Cause: xx509cert is just a set of pointers into the DER. Once the DER buffer (whether a PEM decode product or file contents read in) is freed, all views dangle.

错误写法
xx509cert* load_cert(const char* sPemText) {
	bytes pDer = ...;                    /* local DER buffer */
	xx509cert* pCert = (xx509cert*)xrtMalloc(sizeof(*pCert));
	xrtX509Parse(pDer, iSize, pCert);
	return pCert;   /* pDer's lifetime ends: every returned view dangles */
}
正确写法
/* whoever holds the DER holds the views — bind buffer and view in the same scope */
typedef struct { bytes pDer; size_t iSize; xx509cert Cert; } loadedcert;
bool load_cert(const char* sPemText, loadedcert* pOut) {
	
	   the two live and die together, released/held by the caller as one */
}
Pitfall 2: taking Common Name as the service identity:Pitfall 2: taking Common Name as the service identity

Symptoms: the certificate's SAN has api.example.test but the CN is Example Ltd — code matching by CN either fails to match (good case) or gets deceived by a malicious certificate's CN field (bad case): the attacker self-signs a certificate whose CN states the target domain.

Cause: RFC 9525 abolished the CN fallback — service identity reads only SubjectAltName's DNS-ID/IP-ID. CN is a human-facing display name that certificate authors fill freely.

错误写法
/* walk Subject for the CN attribute and compare its value with the hostname */
if ( strcmp(cn_value, sHost) == 0 ) { accept(); }
/* the CN is entirely controlled by the certificate author: this is not identity verification */
正确写法
/* identity matching goes through SAN: xrtX509MatchHost (Chapter 80) reads DNS-ID/IP-ID only */
xx509genname Presented;
if ( xrtX509MatchHost(&Cert, XRT_STR_LITERAL(sHost),
		&Presented) != X509_VALUE ) {
	reject();   /* SAN miss: reject, never fall back to CN */
}
Pitfall 3: treating Version as a bitmask or comparing as string:Pitfall 3: treating Version as a bitmask or comparing as string

Symptoms: Version == 3 never true (v3's enum value is 3 but written as a string/bit test), or walking the Extensions view of a v1 certificate as empty and iterating anyway.

Cause: the version is a natural-numbered enum (v1=1, v2=2, v3=3), and an absent field means v1 — old certificates carry no explicit version byte. Extensions exist only in v3; walking extensions of v1/v2 certificates requires a version check first.

错误写法
if ( Cert.Version & 3 ) {          /* bit test: semantics completely wrong */
	xrtX509ExtensionInit(&Cert, &Cursor);   /* v1 certificate: no extensions */
	while ( /* iterate */ );
}
正确写法
if ( Cert.Version >= X509_VERSION_3 ) {
	/* v3: extensions exist — traverse safely */
} else {
	
	   necessarily fails; such certificates deserve rejection in modern TLS */
}

Programming Exercises

Basic: read all the inspect certificate's facts

Extend the inspect example: besides version/subject/public key, also print the Issuer's first RDN, the years of NotBefore/NotAfter, and the result of xrtX509ValidAt(&Cert, xrtNow()). Acceptance: consistent with hand-derived DER (against RFC 5280's structure diagram); the toy certificate's validity conclusion (very likely "expired") explainable.

Advanced: a serial-and-algorithm browser

Write a function dump_cert(loadedcert* pC): print the serial (HEX, byte by byte), the signature-algorithm translation (type/hash/salt length), and the public-key type enum name. Generate three different certificates with OpenSSL (RSA-PSS, ECDSA P-256, Ed25519 one each), run on each, and compare with openssl x509 -text. Acceptance: all three certificates' algorithm parameters agree with OpenSSL's display; serials byte-for-byte identical (leading zeros preserved).

Challenge: an extension-inventory tool

Implement a "certificate checkup": walk all extensions, outputting OID, critical bit, and a semantic summary of recognized extensions (SAN's DNS list, BasicConstraints' CA flag and depth, KeyUsage bitmap, CRL distribution-point URLs); mark unrecognized ones unknown. Use it to inspect a real site certificate. Acceptance: recognized summaries match openssl x509 -text; critical-and-unrecognized extensions get a highlighted warning (Chapter 81 explains why this should fail verification).

Summary

TopicQuick reference
StructureCertificate = tbsCertificate + signatureAlgorithm + signatureValue (signature over the TBS original)
One-step parsingxrtX509Parse(DER, 长度, &视图); zero allocation, zero frees; structural validation built in
View fieldsRaw/Tbs/Version/Serial/Issuer/Subject/NotBefore/NotAfter/SPKI/Extensions/Signature
Versionnatural numbering (v1=1 default / v2=2 / v3=3); extensions only in v3
Validityxtime closed interval; xrtX509ValidAt(&cert, xrtNow()) in one step; expired ≠ error
Serialoriginal content bytes (sign bit and overlong values preserved); CA-side constraints enforced separately
Name cursorNameInit/Read per RDN attribute; equality by DER bytes; identity never reads CN
Public keyxrtX509PublicKey → type enum + material; type independent of compiled verification backends
Algorithm translationAlgorithmParse+SignatureParse → normalized parameters (type/hash/mgf/salt/trailer)
Extensionscursor traversal; SAN/BasicConstraints/NameConstraints/CRL extensions have dedicated entries
View lifetimebound to the input DER buffer; in wrappers, buffer and view live and die together

📖 API references in this chapter:x509 (140 entries)asn1 (35 entries)pem (11 entries)

Click for full signatures, parameter constraints, return values and error notes