Schema engine

L2 of the ASN.1 stack -- a declarative structure-schema engine. A schema is plain data ({kind, ...} descriptors built by the combinators here) and walk(schema, node, ctx) interprets it against a decoded DER node. The engine is where every cross-cutting structural rule lives ONCE: the shape assertion (SEQUENCE / SET / bare-constructed), bounds-checked positional reads, optional and context-tagged fields in strictly-increasing tag order, SET-OF uniqueness, and fail-closed typed errors. A format module declares a schema and calls walk under an error namespace ctx = { E, prefix, oid }; it never hand-rolls children[idx++], so the positional-read and duplicate-field bug classes are structurally retired. This is the shared base the certificate parser (and, later, CRL / CMS) composes.

pki.schema.engine.walk

since 0.1.7 stable
pki.schema.engine.walk(schema, node, ctx) -> value

Interpret a declarative schema against a decoded DER node, enforcing the schema's structural rules (shape assertion, arity, optional / context-tagged fields in increasing tag order, SET-OF uniqueness) and returning the built value -- or the match tree ({ node, fields | items }, with the build output on .result) for a structure with no build fn. ctx = { E, prefix, oid } supplies the typed-error constructor, the error-code family prefix, and the OID registry a build fn resolves names through.

The schema is assembled from the combinators this module exports: structural (seq / field / optional / explicit / trailing / seqOf / setOf / setOfUnique / implicitSeqOf / implicitSetOf / choice) and value (oidLeaf / integerLeaf / boolean / octetString / bitString / implicitBitString / implicitOctetString / implicitNull / implicitInteger / any / decode / time).

Example

var S = pki.schema.engine;
// `E` is an error FACTORY (called without `new`), so a domain can raise its own type
var MyError = function (code, msg) { var e = new Error(msg); e.code = code; return e; };
var der = pki.asn1.build.sequence([pki.asn1.build.oid("1.3.101.112")]);
var ALGID = S.seq([S.field("algorithm", S.oidLeaf())],
  { assert: "sequence", arity: { min: 1 }, code: "app/bad-alg" });
S.walk(ALGID, pki.asn1.decode(der), { prefix: "app", E: MyError, oid: pki.oid });

References

pki.schema.engine.encode

since 0.1.17 stable
pki.schema.engine.encode(schema, value, ctx) -> Buffer

Encode a structural value to canonical DER by interpreting the same schema walk decodes, in the constructor direction. value mirrors the schema: a seq takes { fieldName: value }, a leaf its natural JS value (an OID string, a BigInt, a { unusedBits, bytes } BIT STRING, a Date), a repeat an array, a choice { arm, value }. EXPLICIT wrappers and IMPLICIT [tag] retagging are applied by the engine, so walk(schema, decode(encode(schema, v))) round-trips.

Example

var S = pki.schema.engine;
var der = S.encode(S.seq([S.field("n", S.integerLeaf())]), { n: 42n });

References

pki.schema.engine.embeddedDer

since 0.1.18 stable
pki.schema.engine.embeddedDer(schema, bytes, ctx, opts?) -> value

Decode a fresh DER (or, with ber: true, BER) blob carried inside an already-decoded value -- an OCTET STRING whose content is itself an encoded structure -- and walk it against a schema. A codec failure is wrapped in the caller's typed code; a schema rejection keeps its own code. This is the one named form of the re-decode idiom, so the caps that a fresh pki.asn1.decode would restart from zero can be carried across re-decode boundaries: a shared budget ({ remaining: n }) decrements on every call and fails with budgetCode at zero, bounding how many nested blobs one parse may unwrap however deeply a container chains them.

Options

code:       string,   // typed code wrapping a codec failure (required)
what:       string,   // human label for the wrapped message
ber:        boolean,  // default false; BER content region (RFC 7292 sec. 4.1)
budget:     object,   // { remaining: n } shared across a parse's re-decodes
budgetCode: string,   // typed code when the budget is exhausted

Example

var S = pki.schema.engine;
var MyError = function (code, msg) { var e = new Error(msg); e.code = code; return e; };
var INNER = S.seq([S.field("version", S.integerLeaf())], { code: "app/bad-inner" });
var ns = { prefix: "app", E: MyError, oid: pki.oid };
S.embeddedDer(INNER, pki.asn1.build.sequence([pki.asn1.build.integer(3n)]), ns,
  { code: "app/bad-der", what: "the embedded structure" });

References

  • spec X.690
  • defends ASN.1-parser-DoS (CWE-400)