X.509 attribute certificates (RFC 5755)

The RFC 5755 attribute-certificate producing side. 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

since 0.3.2 stable
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

since 0.5.15 stable
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