Hardware key vault

Every secret SuperDRM holds — content keys, tenant token secrets, Widevine signing keys, FairPlay material — is stored sealed (AES-256-GCM, bound to the row it belongs to) under a Key-Encryption Key (KEK). The KEK never touches the database. Where the KEK lives is the only thing that changes between tiers, and it is chosen with one variable: SUPERDRM_VAULT.

#What "hardware-backed" means at each tier — honestly

TierSUPERDRM_VAULTWhere the KEK isWho can read a content key in plaintextUse for
SoftwarelocalA 32-byte file on disk (chmod 600)Anyone who can read the file and the databaseDev, single-box staging. Not a security boundary.
HSMpkcs11 or aws-kmsInside the HSM, CKA_EXTRACTABLE=false (or a KMS CMK). The api only asks the HSM to encrypt or decrypt.The api process, for the milliseconds a licence needs it; a database dump alone is uselessProduction licence servers. Stealing the database does not steal the keys.
Enclavepkcs11 / aws-kms inside an attested enclave (AWS Nitro Enclave, GCP Confidential VM, IBM Hyper Protect)As above, and the HSM/KMS policy only releases to a measured enclave imageOnly code whose attestation matchesContracts that require "keys never in plaintext on a general-purpose host" — the FairPlay KSM and the Widevine signing key live here.

The api code is identical across tiers. What you gain by going up a tier is a smaller set of parties who could ever see plaintext, and an audit trail that says so.

#Supported PKCS#11 tokens

TokenModuleNotes
YubiHSM 2 (FIPS 140-2 L3 variant available)/usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so (aarch64: /usr/lib/aarch64-linux-gnu/pkcs11/yubihsm_pkcs11.so), with yubihsm-connector runningPIN is <auth key id><password>, e.g. 0001password. Set YUBIHSM_PKCS11_CONF to point at the connector. One on the licence node, one in a safe.
Thales Luna (Network / PCIe / Cloud HSM)/usr/safenet/lunaclient/lib/libCryptoki2_64.soPartition label → SUPERDRM_PKCS11_TOKEN_LABEL; crypto-officer password → PIN.
AWS CloudHSM/opt/cloudhsm/lib/libcloudhsm_pkcs11.soPIN is CU_user:password. Alternatively use aws-kms (cheaper, same FIPS 140-3 L3 boundary, no cluster to run).
SoftHSM2/usr/lib/softhsm/libsofthsm2.soSoftware emulation of a PKCS#11 token. CI and tests onlynpm run test:pkcs11 proves the code path here.

aws-kms uses the KMS API directly (no PKCS#11) with EncryptionContext = {aad}, which gives the same binding as GCM's AAD.

#Environment

Environment
SUPERDRM_VAULT=pkcs11
SUPERDRM_PKCS11_MODULE=/usr/lib/aarch64-linux-gnu/pkcs11/yubihsm_pkcs11.so
SUPERDRM_PKCS11_PIN=0001<password>
SUPERDRM_PKCS11_KEY_LABEL=superdrm-kek-1        # becomes kek_id "pkcs11:superdrm-kek-1"
SUPERDRM_PKCS11_TOKEN_LABEL=                     # optional; else slot index
SUPERDRM_PKCS11_SLOT=0                           # optional
SUPERDRM_PKCS11_GCM_PARAMS=v240                  # v230 only for a legacy token without ulIvBits
SUPERDRM_KEK_ID=                                 # optional override of the recorded kek_id

SUPERDRM_VAULT=aws-kms
SUPERDRM_KMS_KEY_ID=arn:aws:kms:us-west-2:…:key/…
AWS_REGION=us-west-2

On first start the vault generates the KEK inside the token if no object with that label exists (AES-256, CKA_TOKEN, CKA_SENSITIVE, CKA_EXTRACTABLE=false, encrypt/decrypt only). Every later start finds it by label. GET /health reports vault.provider and vault.detail.

#Key ceremony

  1. Two custodians, one session, written minutes. Nobody holds the whole token PIN alone: custodian A types the first half, custodian B the second (YubiHSM: two auth keys with split capabilities is the cleaner version).
  2. Generate in-token. Start the api once with the new label; confirm with pkcs11-tool --module … --login --list-objects that the object shows never extractable and sensitive. Never import a KEK you generated elsewhere.
  3. Back up using the token's own mechanism, never by exporting: YubiHSM wrap the KEK under a wrap key whose halves the custodians hold; Luna partition backup to a Backup HSM; CloudHSM cluster backup to S3. Store the wrapped blob in a sealed envelope in a safe, signed by both custodians, with the kek_id and date on the outside.
  4. Record kek_id, module, token serial, custodians and the backup location in the change log (audit_log row vault.ceremony via superdrm audit, plus the paper minutes).
  5. Prove restore once a year: restore the backup to a spare token in a staging box and open one sealed row with it.

#Rotation

Rotation is a new KEK, not a re-keyed one. Sealed rows carry kek_id, so old and new can coexist.

  1. Generate the new KEK (ceremony above) with a new label, e.g. superdrm-kek-2.

  2. Point the api at the new label (SUPERDRM_PKCS11_KEY_LABEL=superdrm-kek-2) and restart: new rows seal under the new KEK immediately.

  3. Re-seal the old rows lazily or in one pass:

    Shell
    SUPERDRM_OLD_VAULT=pkcs11 SUPERDRM_OLD_PKCS11_MODULE=… SUPERDRM_OLD_PKCS11_PIN=… \
    SUPERDRM_OLD_PKCS11_KEY_LABEL=superdrm-kek-1 \
    npm run reseal -- --from pkcs11:superdrm-kek-1 --dry-run
    npm run reseal -- --from pkcs11:superdrm-kek-1

    The re-seal walks content_keys, tenants, tenant_drm and partner_creds; opens each blob with the old vault, seals with the new, verifies by opening it again, and updates in batches of one transaction each. It is idempotent — rows already under the active kek_id are skipped — so it can be re-run after an interruption. The same command moves a deployment from local to an HSM (SUPERDRM_OLD_VAULT=local SUPERDRM_OLD_LOCAL_KEK_FILE=…).

  4. When select count(*) from content_keys where kek_id = '<old>' is zero, destroy the old KEK object in the token and shred its backup envelope (both custodians, minuted).

#Proving it

npm run test:pkcs11 builds a clean node:24-bookworm container, installs SoftHSM2 and pkcs11js, initialises a token, then runs the vault tests (KEK attributes, seal/open, AAD and tamper rejection, re-open finds the key, foreign-label refusal) and the entire api test suite on the PKCS#11 vault. It ends with pkcs11-tool --list-objects, an independent tool, showing the KEK as never extractable. Swap the module path and PIN and the same test proves a real YubiHSM.

Updated September 2026