X.509 attribute certificates (RFC 5755)
pki.attrcert.sign builds an AttributeCertificateInfo binding a Holder to a set of privilege attributes (role, clearance, group, ...) over a validity window, signs it with an Attribute Authority's private key, and emits an AttributeCertificate that pki.schema.attrcert.parse accepts and re-validates byte for byte. Unlike a public-key certificate an attribute certificate is never self-signed: the holder has no key, so the issuing AA is always a distinct signer. Parsing lives at pki.schema.attrcert.parse.pki.attrcert.sign
pki.attrcert.sign(spec, issuer, opts?) -> Promise<Buffer|string>
Build, sign, and DER-encode an RFC 5755 attribute certificate as an Attribute Authority. spec describes the certificate: holder (exactly one form: entityName, baseCertificateID, fromCertificate to bind a public-key certificate's identity, or objectDigestInfo), notBeforeTime / notAfterTime (Dates -> GeneralizedTime), an optional serialNumber (positive, <= 20 octets; a random 20-octet serial is generated when omitted), attributes (an object of the sec. 4.4 privilege syntaxes (role / clearance / group / chargingIdentity / accessIdentity / authenticationInfo), or an array of pre-encoded Attribute DER), and optional extensions (an object of auditIdentity / targetInformation / noRevAvail / aaControls / acProxying / authorityKeyIdentifier / cRLDistributionPoints / authorityInfoAccess, the last two in the list forms pki.x509.sign takes, or an array of pre-encoded Extension DER). issuer is the signing AA: { cert, key } (the AA certificate DER/PEM and its private key) or { name, publicKey, key } (an explicit issuer DN, AA SPKI DER, and key); an attribute certificate is never self-signed. RFC 5755 section 6 defines two revocation schemes and makes them exclusive ("An AC MUST NOT contain both a noRevAvail extension and a 'pointer in AC'"), so noRevAvail beside a cRLDistributionPoints or authorityInfoAccess extension is refused, in the named form and the pre-encoded form alike.
The authorityKeyIdentifier is emitted without being named (section 4.3.3: "this extension SHOULD be included in ACs"); its keyIdentifier is the AA certificate's subjectKeyIdentifier when issuer.cert is given, else the RFC 5280 method (1) value of the AA key, and a stated one is held to the certificate's. authorityKeyIdentifier: false omits it. The pre-encoded array form emits exactly what it is given. Both forms are held to the rules section 4.3 and 4.4 place on the issuer, read off the decoded value: an auditIdentity is 1 to 20 octets (4.3.1); a targetInformation carries one Targets element and never a targetCert (4.3.2); an id-ad-ocsp accessLocation is a URI carrying an HTTP URL (4.3.4); a cRLDistributionPoints names one distribution point as a fullName holding a single directoryName or an HTTP or LDAP URL, and a cRLIssuer beside it names the CRL issuer as a directoryName (4.3.5, RFC 5280 sec. 4.2.1.13); and a role's roleName is a uniformResourceIdentifier (4.4.5). A violation is refused with attrcert/bad-input. The signature algorithm is resolved from the AA key (RSA PKCS#1 v1.5 or PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm), and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM ATTRIBUTE CERTIFICATE with opts.pem. Malformed input throws a typed AttrCertError; where the spec carries raw DER (a holder or issuer Name Buffer, a pre-encoded Extension) a malformed leaf inside those bytes throws Asn1Error. The AA certificate's own profile (RFC 5755 sec. 4.5) and validity are a verification-layer concern, so validate the AA certificate with pki.path.validate before trusting the attribute certificate. Parsing is pki.schema.attrcert.parse.
Options
- `pem` (boolean) -- return a PEM `ATTRIBUTE CERTIFICATE` string instead of DER.
- `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
- `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
var signerCertDer = await pki.x509.sign({ subject: "Attribute Authority", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: signerKeyPkcs8 });
var ac = await pki.attrcert.sign(
{ holder: { entityName: { directoryName: "CN=Alice" } },
notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
{ cert: signerCertDer, key: signerKeyPkcs8 });
pki.schema.attrcert.parse(ac).attributes[0].type; // the role attribute OID
}
example();
References
- spec RFC 5755
- defends
forged-attribute-certificate (CWE-347)
pki.attrcert.verify
pki.attrcert.verify(ac, issuer, opts) -> Promise<{ valid, verified, signatureValid, validityChecked, targetingChecked, revocationChecked, noRevAvail, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }>
Verify an RFC 5755 attribute certificate. An AC carries privilege attributes (a role, a group, a clearance), so a consumer that reads them without this grants privileges on unauthenticated input. ac is a DER Buffer, a PEM string, or a parsed attribute certificate; issuer is { name, publicKey }, the AC issuer this verifier trusts as an AC issuer, which section 5 item 4 makes the verifier's own configuration and this verb therefore refuses to infer from the AC. issuer.name takes every form pki.attrcert.sign accepts for the issuing AA (a string, an array of RDNs, or raw Name DER) and is compared as a distinguished name (RFC 5280 section 7.1), so an AC issued under a multi-RDN authority name is verifiable under that same name.
Both options are read at the call, before signature verification suspends it, so a caller that reuses or rewrites its options object (including calling setTime on the Date it passed in) cannot change the verdict that call returns.
The checks it performs are section 5's items 2, 4, 5, 6 and 7: the signature over the exact AttributeCertificateInfo bytes through the one path-validation signature engine; the issuer being the one named; the evaluation instant lying within the validity, where equality with either bound succeeds as the section states; the targeting rule of section 4.3.2, where an AC naming targets is refused at a verifier it does not name; and rejection of any critical extension this verb does not process. Section 5 defines support as parsing the value AND rejecting where the value would reject, so an extension parsed but never evaluated is not supported: targeting is processed and an audit identity states no rule that rejects, while aaControls and acProxying carry constraints this verb does not evaluate, so a critical one is refused. A critical extension defined in future defaults to refused. The verdict carries extensions so an audit identity reaches the caller for the logging section 4.3.1 asks for.
Targeting compares opts.target against the targetName and targetGroup alternatives only, under each GeneralName form's own matching rule: a dNSName folds case across the whole name (RFC 5280 sec. 7.2), a mailbox matches its local-part exactly and its host-part case-insensitively (sec. 7.5), and a directoryName goes through the same distinguished-name comparison the rest of the toolkit uses (sec. 7.1). A URI compares as encoded, because sec. 7.4 makes URI equality a full RFC 3987 normalization (percent-encoding, path segments and scheme-based rules) and a partial one could match a URI it should not. A form with no comparison here (otherName, ediPartyName, x400Address) leaves targetingChecked: false and never a pass. Every one of those limits can refuse a target a fuller comparison would accept, and none can accept one it would refuse. Re-open condition: an RFC 3987 normalizer, at which point URIs compare under sec. 7.4.
The third Target alternative, targetCert, is one section 4.3.2 says "MUST NOT be used", so an attribute certificate carrying one is refused before any match is considered, with targetingChecked: false. A match on some other entry does not rescue it: the issuer broke a MUST NOT of its own profile, and letting the verdict turn on which entries rode alongside the forbidden one would leave the outcome to whoever assembled the certificate.
Revocation is section 6, outside the seven. This verb implements that section's "never revoke" scheme, which AC users MUST support: it holds no revocation evidence and follows no pointer out of the certificate. The section states the consequence, "If only the 'never revoke' scheme is supported, then all ACs that do not contain a noRevAvail extension, MUST be rejected", because an issuer that omits noRevAvail is stating that revocation status checks are supported, and a verdict that skipped one would grant privileges the issuer expected to be able to withdraw. So an AC carrying noRevAvail verifies with revocationChecked: true and noRevAvail: true, and one without it is refused unless the caller supplies opts.revocationStatus. A caller running the section's "pointer in AC" scheme reads the certificate's own cRLDistributionPoints / authorityInfoAccess from the verdict's extensions (each carried decoded, as the RFC 5280 readers return them), establishes the status, and passes it back.
Items 1 and 3, and the issuer path in item 2, need certificates this verb is not given: the holder's own public-key certificate and its chain, the AC issuer's chain, and the section 4.5 profile of the issuer's certificate. Run those through pki.path.validate with the certificates you hold. The verdict reports them as holderBindingChecked: false and issuerPathChecked: false rather than leaving their absence to read as a pass, and verified never means more than the checks whose slots are true. Re-open condition: when a caller can pass the holder and issuer certificates, both become checks this verb performs and both slots follow the arguments.
Options
time the instant to evaluate the AC at. Omitting it leaves section 5 item 5 unasked, which
reports `validityChecked: false` and never `verified: true`.
target a GeneralName naming this verifier, for the section 4.3.2 targeting check.
revocationStatus
`"notRevoked"` or `"revoked"`, the status the caller established for this AC through
the section 6 "pointer in AC" scheme. Supplying it answers section 6 for an AC that
carries no `noRevAvail`; omitting it leaves such an AC refused.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var aaSpki = await pki.key.export(pair.publicKey);
var aaKey = await pki.key.export(pair.privateKey);
var acDer = await pki.attrcert.sign({
holder: { entityName: { directoryName: "CN=Alice" } },
notBeforeTime: new Date("2026-01-01T00:00:00Z"),
notAfterTime: new Date("2027-01-01T00:00:00Z"),
attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } },
// RFC 5755 sec. 6: the issuer states no revocation information will exist for this AC.
extensions: { noRevAvail: true },
}, { name: "CN=Example AA", publicKey: aaSpki, key: aaKey });
var r = await pki.attrcert.verify(acDer, { name: "CN=Example AA", publicKey: aaSpki },
{ time: new Date("2026-06-01T00:00:00Z") });
r.verified; // true when every check whose slot is true passed
r.attributes; // the privileges, re-derived from the signed bytes
}
example();
References
- spec RFC 5755 sec. 5
- defends
attribute-certificate-forgery (CWE-347)
pki.schema.attrcert.parse
pki.schema.attrcert.parse(input, caps?) -> attributeCertificate
Parse a DER Buffer or a PEM string/Buffer (label ATTRIBUTE CERTIFICATE, the OpenSSL armor) into a structured v2 attribute certificate: { version, holder, issuer, signatureAlgorithm, serialNumber, serialNumberHex, validity, attributes, issuerUniqueID, extensions, tbsBytes, signatureValue }. The holder / issuer identities come back as validated GeneralNames (each element { bytes, tagClass, tagNumber }); the validity window is real Dates; tbsBytes is the exact signed byte range for a verifier.
Throws AttrCertError when the bytes are not a well-formed attribute certificate (an obsolete AttributeCertificateV1 throws attrcert/legacy-v1-not-supported) and Asn1Error when the underlying DER is malformed.
Options
- `maxBytes` / `maxDepth` / `maxItems` (number) -- decode caps for this parse. Each
defaults to the matching `pki.C.LIMITS` figure and may only be set lower; a value
above it, or an option outside this set, is refused. A parse that exceeds one is
refused with the `/too-large` code of this format.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var aaCert = await pki.x509.sign({ subject: "Attribute Authority", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
var der = await pki.attrcert.sign(
{ holder: { entityName: { directoryName: "CN=Alice" } },
notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
{ cert: aaCert, key: key });
var ac = pki.schema.attrcert.parse(der);
ac.attributes[0].name; // "role"
ac.validity.notAfterTime;// Date
}
example();
References
pki.schema.attrcert.pemDecode
pki.schema.attrcert.pemDecode(text, label?) -> Buffer
Extract the DER bytes from a PEM block (default label ATTRIBUTE CERTIFICATE, the OpenSSL armor and the canonical-label default every sibling format applies). Pass a label to enforce a different block type, or an explicit null to accept a block of any type. The text holds one object: a file of several is refused with pem/multiple-blocks and read with pki.schema.pem.decodeBundle. Throws PemError on a missing / mismatched envelope or a non-base64 body.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var aaCert = await pki.x509.sign({ subject: "Attribute Authority", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
var pemText = await pki.attrcert.sign(
{ holder: { entityName: { directoryName: "CN=Alice" } },
notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
{ cert: aaCert, key: key }, { pem: true });
var der = pki.schema.attrcert.pemDecode(pemText);
}
example();
References
pki.schema.attrcert.pemEncode
pki.schema.attrcert.pemEncode(der, label?) -> string
Wrap DER bytes in a PEM envelope (default label ATTRIBUTE CERTIFICATE, the same default pemDecode reads). Throws PemError on a malformed label.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var aaCert = await pki.x509.sign({ subject: "Attribute Authority", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
var der = await pki.attrcert.sign(
{ holder: { entityName: { directoryName: "CN=Alice" } },
notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
{ cert: aaCert, key: key });
var pem = pki.schema.attrcert.pemEncode(der);
}
example();
References
pki.schema.attrcert.decodeExtensions
pki.schema.attrcert.decodeExtensions(acOrBytes, opts?) -> rows
Read an attribute certificate's extensions as decoded records. The argument is the object parse returned or the bytes parse takes.
Each row is { oid, name, critical, value, scope, index, containerIndex, state, decoded, code, profile }, with scope "attribute-certificate" and containerIndex null. state is the discriminator, and profile reports the criticality RFC 5755 fixes for the extension.
RFC 5755 sec. 5 item 7 has a verifier reject an AC carrying a critical extension it does not recognize. That is the caller's rule to apply, and the two facts it needs, critical and state, are on the row.
Options
- `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
undecodable value. It does not turn an unrecognized OID into a throw.
- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var aaCert = await pki.x509.sign({ subject: "Example AA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
var acDer = await pki.attrcert.sign({ holder: { entityName: [{ directoryName: "CN=Holder" }] },
attributes: { role: { roleName: "https://example.test/role/admin" } }, serialNumber: 1n,
notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
extensions: { noRevAvail: true } }, { cert: aaCert, key: key });
var rows = pki.schema.attrcert.decodeExtensions(acDer);
rows.filter(function (r) { return r.critical && r.state === "unrecognized"; }).length; // -> 0
}
example();
References
- spec RFC 5755 sec. 4.2.9
- spec RFC 5755 sec. 4.3
pki.schema.attrcert.decodeExtension
pki.schema.attrcert.decodeExtension(ext, opts?) -> row
Read one attribute-certificate extension record as a decoded row, at scope "attribute-certificate". The argument is { oid, name, critical, value }.
Options
- `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
undecodable value.
- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var aaCert = await pki.x509.sign({ subject: "Example AA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
var acDer = await pki.attrcert.sign({ holder: { entityName: [{ directoryName: "CN=Holder" }] },
attributes: { role: { roleName: "https://example.test/role/admin" } }, serialNumber: 1n,
notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
extensions: { noRevAvail: true } }, { cert: aaCert, key: key });
var ac = pki.schema.attrcert.parse(acDer);
pki.schema.attrcert.decodeExtension(ac.extensions[0]).profile.citation; // -> "RFC 5755 sec. 4.3.6"
}
example();
References
- spec RFC 5755 sec. 4.3