npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@durin/aliro-cose

v0.1.0

Published

CBOR encoding, COSE signing, and Aliro-specific data structure assembly for the Aliro access control protocol

Readme

@durin/aliro-cose

Cryptographic core of the Aliro access-control protocol. Encodes, signs, and verifies Access Documents and Revocation Documents using CBOR (RFC 8949), COSE Sign1 (RFC 9052), and Aliro's ISO 18013-5-based key remapping (Aliro §7).

Contents


Installation

pnpm add @durin/aliro-cose
# or
npm install @durin/aliro-cose

Requires Node.js 18+.


Quick start

import {
  generateCredentialKeyPair,
  computeKid,
  buildAccessData,
  createAccessDocument,
  signAccessDocument,
  verifyAccessDocument,
  ALIRO_DOC_TYPE_ACCESS,
} from '@durin/aliro-cose';

// 1. Generate a key pair for the issuer (or load from your KMS)
const issuerKeys = generateCredentialKeyPair();

// 2. Generate a key pair for the User Device credential
const deviceKeys = generateCredentialKeyPair();

// 3. Build a minimal access data element
const accessData = buildAccessData({ version: 1 });

// 4. Assemble the unsigned Access Document
const now = new Date();
const doc = createAccessDocument({
  elements: [{ identifier: 'primary_access', accessData }],
  validityInfo: {
    signed: now,
    validFrom: now,
    validUntil: new Date(now.getTime() + 365 * 86_400_000), // 1 year
  },
  credentialPublicKey: deviceKeys.publicKey,
  timeVerificationRequired: false,
});

// 5. Sign with the issuer's private key
const kid = computeKid(issuerKeys.publicKey);
const coseSign1Bytes = await signAccessDocument(doc, issuerKeys.privateKey, { kid });

// 6. Verify (Reader side)
const result = verifyAccessDocument(coseSign1Bytes, issuerKeys.publicKey, doc.issuerSignedItems);
console.log(result.valid); // true

Concepts

Document structure

An Aliro Access Document is a COSE_Sign1 envelope (RFC 9052 §4.2) containing:

COSE_Sign1 = [
  protectedHeader,  // CBOR-encoded map: { alg: ES256, kid/x5chain }
  {},               // unprotected header (empty in Aliro)
  payload,          // CBOR-encoded MobileSecurityObject (MSO)
  signature,        // 64-byte ES256 r‖s compact signature
]

The MSO payload contains:

  • valueDigests — SHA-256 of each IssuerSignedItem (the integrity binding)
  • deviceKeyInfo — the User Device's P-256 public key
  • validityInfo — signed/validFrom/validUntil timestamps + optional iteration counter
  • docType"aliro-a" (Access) or "aliro-r" (Revocation)
  • timeVerificationRequired — whether the Reader must validate timestamps

Each IssuerSignedItem wraps a single data element (access rules, schedules, etc.) with a random salt and sequential digest ID. Items can be selectively disclosed to the Reader.

Key remapping

Aliro replaces ISO 18013-5 human-readable map keys with short numeric string keys ("1", "2", …) encoded as CBOR text strings — not integers. This library handles all remapping internally; you work with plain TypeScript objects.


Issuing an Access Document

1. Build data elements

Minimal element

import { buildAccessData } from '@durin/aliro-cose';

const accessData = buildAccessData({ version: 1 });

With access rules and schedules

import {
  buildAccessData,
  buildAccessRule,
  buildSchedule,
  buildRecurrenceRule,
} from '@durin/aliro-cose';

// A schedule: weekday business hours, UTC
const schedule = buildSchedule({
  scheduleId: 1,
  startPeriod: Math.floor(Date.now() / 1000),
  endPeriod: Math.floor(Date.now() / 1000) + 365 * 86400,
  flags: { timeInUtc: true },
  recurrenceRule: buildRecurrenceRule({
    pattern: 'Weekly',
    durationSeconds: 9 * 3600, // 9 hours
    interval: 1,
    ordinal: 0,
    mask: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true },
  }),
});

// An access rule: allow secure access during the schedule above
const rule = buildAccessRule({
  capabilities: { secure: true },
  allowScheduleIds: [1],
});

// Build the element
const accessData = buildAccessData({
  version: 1,
  id: new Uint8Array([0x01, 0x02]), // 1–16 bytes, optional
  accessRules: [rule], // max 8 rules
  schedules: [schedule], // max 8 schedules
  readerRuleIds: [100], // max 8, each uint16
  timeVerificationRequired: false, // set via document opts, not here
});

buildAccessRule capabilities (Aliro §7 Table 7-3):

| Field | Meaning | | --------------------------- | --------------------------- | | secure | Lock/unlock when secured | | unsecured | Lock/unlock when unsecured | | toggleSecuredOrUnsecured | Toggle the secured state | | momentaryUnsecure | Momentary unsecure | | extendedMomentaryUnsecure | Extended momentary unsecure |

2. Assemble and sign

import {
  generateCredentialKeyPair,
  computeKid,
  createAccessDocument,
  signAccessDocument,
  ALIRO_DOC_TYPE_ACCESS,
} from '@durin/aliro-cose';

const issuerKeys = generateCredentialKeyPair();
const deviceKeys = generateCredentialKeyPair(); // device generates its own in production

const now = new Date();
const validUntil = new Date(now.getTime() + 30 * 86_400_000); // 30 days

// createAccessDocument returns an unsigned document you can inspect
const doc = createAccessDocument({
  elements: [{ identifier: 'primary_access', accessData }],
  validityInfo: {
    signed: now,
    validFrom: now,
    validUntil,
    validityIteration: 0, // optional; increment on each re-issuance
  },
  credentialPublicKey: deviceKeys.publicKey, // omit for keyless docs
  timeVerificationRequired: true,
});

// Inspect before signing if needed:
console.log(doc.valueDigests.size); // number of IssuerSignedItems
console.log(doc.msoBytes.length); // raw MSO CBOR bytes

// Sign — returns the final COSE_Sign1 bytes
const kid = computeKid(issuerKeys.publicKey);
const coseSign1Bytes = await signAccessDocument(doc, issuerKeys.privateKey, {
  kid, // 8-byte key identifier
  // x5chain: certDerBytes,  // optionally include DER-encoded certificate chain
});

Header options — at least one of kid or x5chain is required:

| Option | Type | Description | | --------- | ---------------------- | --------------------------------- | | kid | Uint8Array (8 bytes) | Key identifier (use computeKid) | | x5chain | Uint8Array | DER-encoded issuer certificate |

3. Build a provisioning payload

To deliver the credential to the User Device, build a JSON-serializable payload:

import { buildProvisioningPayload, ALIRO_DOC_TYPE_ACCESS } from '@durin/aliro-cose';

const payload = buildProvisioningPayload({
  docType: ALIRO_DOC_TYPE_ACCESS,
  issuerAuth: coseSign1Bytes, // the signed document
  issuerSignedItems: doc.issuerSignedItems,
});

// payload is JSON-serializable — base64 encodes all binary fields
const json = JSON.stringify(payload);
// Send over QR code, BLE OOB, server push, etc.

// payload shape:
// {
//   docType: 'aliro-a',
//   issuerAuthBase64: '<base64>',
//   items: [
//     { identifier: 'primary_access', digestId: 0, itemBytesBase64: '<base64>' },
//   ],
// }

Security note: The device's private key is generated inside the secure enclave and never leaves it. This library never receives or stores device private keys in production.


Issuing a Revocation Document

import {
  buildRevocationData,
  createRevocationDocument,
  signRevocationDocument,
  ALIRO_DOC_TYPE_REVOCATION,
} from '@durin/aliro-cose';

// Overwrite mode: replace the entire revocation list
const revocationData = buildRevocationData({
  changeMode: 0, // 0 = Overwrite, 1 = Update
  revocationEntries: [
    { publicKeyHash: sha256OfRevokedKey }, // SHA-256 of credential public key
    { keyIdentifier: revokedKid }, // or use the 8-byte KID
  ],
});

const revDoc = createRevocationDocument({
  elements: [{ identifier: 'revocation_list', accessData: revocationData }],
  validityInfo: { signed: now, validFrom: now, validUntil },
  timeVerificationRequired: false,
  // No credentialPublicKey — Revocation Documents never include deviceKeyInfo
});

const revBytes = await signRevocationDocument(revDoc, issuerKeys.privateKey, { kid });

Update mode (append/remove from existing list):

const updateData = buildRevocationData({
  changeMode: 1, // Update
  revocationEntries: [
    { publicKeyHash: newlyRevokedKey }, // entries to add
  ],
  entriesToRemove: [
    { publicKeyHash: restoredKey }, // entries to remove (Update mode only)
  ],
});

Reader-side verification

import { verifyAccessDocument } from '@durin/aliro-cose';

const result = verifyAccessDocument(
  coseSign1Bytes, // bytes received from the User Device
  issuerPublicKey, // 65-byte uncompressed P-256 public key
  issuerSignedItems, // items presented by the device
  new Date(), // currentTime (optional, defaults to Date.now())
  storedIteration, // Reader's stored validityIteration (optional)
);

if (result.valid) {
  console.log('Access granted');
} else {
  console.log('Access denied:', result.reason);
}

// Inspect each verification step:
const { steps } = result;
steps.structureValid; // COSE_Sign1 is well-formed
steps.signatureValid; // ES256 signature is valid
steps.digestsValid.allValid; // all item digests match the MSO
steps.validityInfo.valid; // document is within its validity window
steps.validityIteration; // iteration check result (if storedIteration provided)
steps.timeVerificationRequired; // value of the flag in the MSO

Validity iteration rules (Aliro §7.4)

| Condition | Result | | --------------------------- | -------------------------------- | | docIter >= storedIter | Valid (current or ahead) | | storedIter - docIter < 8 | Valid (within tolerance) | | storedIter - docIter >= 8 | Invalid (document too stale) |

Lower-level verification helpers

import {
  verifySignature,
  verifyDigests,
  verifyValidityInfo,
  verifyValidityIteration,
} from '@durin/aliro-cose';

// Check only the COSE signature
const sigOk = verifySignature(coseSign1Bytes, issuerPublicKey);

// Check only the item digests
const digestResult = verifyDigests(issuerSignedItems, valueDigests);
// { allValid: true, details: [{ digestId: 0, valid: true }, ...] }

// Check only the validity window
const validity = verifyValidityInfo(validFrom, validUntil, new Date());
// { valid: true } or { valid: false, reason: '...' }

// Check only the iteration counter
const iterResult = verifyValidityIteration(docIter, storedIter);
// { valid: true } or { valid: false, reason: '...' }

HSM / KMS integration

The signer parameter in signAccessDocument / signRevocationDocument accepts either a raw private key (for development) or an async SignerFunction that delegates signing to an HSM or KMS. The library never sees the private key in the SignerFunction path.

import type { SignerFunction } from '@durin/aliro-cose';

// Production: sign inside your KMS/HSM
const signerFn: SignerFunction = async (data: Uint8Array): Promise<Uint8Array> => {
  // `data` is the RFC 9052 Sig_structure bytes — pass them to your HSM
  const signature = await myKms.sign({ keyId: 'issuer-key-id', data });
  // Must return a 64-byte compact r‖s signature (not DER-encoded)
  return signature;
};

const coseSign1Bytes = await signAccessDocument(doc, signerFn, { kid });

The data passed to SignerFunction is already the hashed + structured bytes per RFC 9052 §4.4. Most KMS APIs require you to pass the raw data to a sign(data, { alg: 'ES256' }) method — the KMS performs the SHA-256 hash internally.


Key utilities

import {
  generateCredentialKeyPair,
  computeKid,
  encodePublicKeyAsCoseKey,
  parseCoseKey,
} from '@durin/aliro-cose';

// Generate a P-256 key pair
const { privateKey, publicKey } = generateCredentialKeyPair();
// privateKey: Uint8Array (32 bytes) — store in HSM
// publicKey:  Uint8Array (65 bytes) — 0x04 || x || y

// Compute the 8-byte key identifier
const kid = computeKid(publicKey);
// kid = SHA-256("key-identifier" || publicKey)[0..8]

// Encode a public key as a CBOR COSE_Key (for embedding in external structures)
const coseKeyBytes = encodePublicKeyAsCoseKey(publicKey);

// Parse a COSE_Key back to an uncompressed public key
const recovered = parseCoseKey(coseKeyBytes);

Testing utilities

Import from the @durin/aliro-cose/testing subpath — these are excluded from the production bundle.

import {
  createTestKeyPair,
  createTestVector,
  makeTestElement,
  simulateReaderVerification,
  fuzzAccessData,
  hexDump,
  compareCBOR,
  prettyPrintCBOR,
} from '@durin/aliro-cose/testing';

Deterministic test key pairs

const { privateKey, publicKey } = createTestKeyPair('my-test-seed');
// Same seed always produces the same key pair — useful for reproducible test vectors

Test vectors

const vector = await createTestVector({
  name: 'access-doc-v1',
  seed: 'stable-seed',
  validityInfo: { signed: now, validFrom: now, validUntil: later },
  elements: [makeTestElement('primary')],
});

// vector.coseSign1Bytes — the signed document
// vector.publicKey      — the issuer's public key (derived from seed)
// vector.msoBytes       — raw MSO for inspection

Reader simulation

const result = simulateReaderVerification(
  coseSign1Bytes,
  issuerPublicKey,
  issuerSignedItems,
  { storedIteration: 3 }, // Reader state
  new Date(), // fixed time for tests
);
// { accessGranted: true, verificationResult: { ... } }

Fuzzing

const result = await fuzzAccessData(10_000); // 10k random iterations
console.log(result.passed); // should equal 10000
console.log(result.errors); // should be []

Diagnostics

// Hex dump
hexDump(bytes); // 'deadbeef...'
hexDump(bytes, { groupBy: 4, uppercase: true }); // 'DEADBEEF C0FFEE00...'

// Compare two CBOR byte sequences
const diff = compareCBOR(a, b);
// { equal: false, difference: 'first difference at byte 12: 0x3a vs 0x3b' }

// Pretty-print decoded CBOR structure
console.log(prettyPrintCBOR(msoBytes));
// {
//   "1": "1.0",
//   "2": "SHA-256",
//   "3": { ... },
//   ...
// }

API reference

Document builders

| Function | Description | | ------------------------------------------------- | --------------------------------------------- | | createAccessDocument(opts) | Assembles unsigned MSO + IssuerSignedItems | | signAccessDocument(doc, signer, headerOpts) | Signs and returns COSE_Sign1 bytes | | createRevocationDocument(opts) | Assembles unsigned Revocation Document | | signRevocationDocument(doc, signer, headerOpts) | Signs and returns COSE_Sign1 bytes | | buildProvisioningPayload(opts) | JSON-serializable payload for device delivery |

Data element builders

| Function | Description | | --------------------------- | -------------------------------------------------- | | buildAccessData(opts) | AccessData element (version, id, rules, schedules) | | buildAccessRule(opts) | AccessRule with capability bitmask | | buildSchedule(opts) | Schedule with optional recurrence | | buildRecurrenceRule(opts) | Recurrence rule (Daily/Weekly/Monthly/Yearly) | | buildRevocationData(opts) | RevocationData for Overwrite or Update mode |

Key utilities

| Function | Description | | ------------------------------------- | ----------------------------------------------- | | generateCredentialKeyPair() | Fresh P-256 key pair | | computeKid(publicKey) | 8-byte key identifier from public key | | encodePublicKeyAsCoseKey(publicKey) | Encode as CBOR COSE_Key | | parseCoseKey(bytes) | Decode COSE_Key back to uncompressed public key |

Verification

| Function | Description | | ------------------------------------------ | -------------------------------------------- | | verifyAccessDocument(...) | Full §7.4 verification with per-step results | | verifySignature(bytes, publicKey) | COSE_Sign1 signature check only | | verifyDigests(items, valueDigests) | Item digest integrity check only | | verifyValidityInfo(from, until, now?) | Timestamp window check | | verifyValidityIteration(docIter, stored) | Iteration counter check |

Constants

| Constant | Value | | ---------------------------- | ----------- | | ALIRO_DOC_TYPE_ACCESS | "aliro-a" | | ALIRO_DOC_TYPE_REVOCATION | "aliro-r" | | ALIRO_NAMESPACE_ACCESS | "aliro-a" | | ALIRO_NAMESPACE_REVOCATION | "aliro-r" |

Testing subpath (@durin/aliro-cose/testing)

| Export | Description | | --------------------------------- | --------------------------------------------- | | createTestKeyPair(seed) | Deterministic P-256 key pair from string seed | | createTestVector(opts) | Signed test document with fixed seed | | makeTestElement(id) | Minimal DataElementInput for test vectors | | simulateReaderVerification(...) | End-to-end Reader simulation | | fuzzAccessData(iterations?) | Random-input round-trip fuzzer | | hexDump(bytes, opts?) | Bytes → hex string with optional grouping | | compareCBOR(a, b) | Byte-level diff of two CBOR sequences | | prettyPrintCBOR(bytes) | Human-readable CBOR decoder |