OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / lib / did-plc.js

57 lines · javascript ref:
// did:plc computation — byte-exact.
//
// A did:plc is derived from its *genesis* operation:
//   did = "did:plc:" + base32( sha256( dag-cbor(signedGenesisOp) ) ).slice(0, 24)
//
// The hash is over the SIGNED op (the `sig` field is included). DAG-CBOR gives a
// canonical byte layout (sorted map keys, definite lengths), so the DID is a pure
// function of the op. base32 here is RFC-4648 lowercase, NO padding, and NO
// multibase prefix (did-method-plc uses raw base32, not multiformats' 'b'-prefixed
// multibase form) — see the golden-vector test.
//
// Reference: https://github.com/did-method-plc/did-method-plc (didForCreateOp)

import * as dagCbor from '@ipld/dag-cbor'
import { sha256 } from '@noble/hashes/sha2'
import { base32 } from 'multiformats/bases/base32'
import { CID } from 'multiformats/cid'
import * as Digest from 'multiformats/hashes/digest'

// multiformats' base32.encode returns a multibase string prefixed with 'b'.
// did:plc uses the raw base32 alphabet output, so strip the leading multibase char.
function rawBase32 (bytes) {
  const multibase = base32.encode(bytes) // 'b' + <rfc4648-lower-nopad>
  return multibase.slice(1)
}

// Canonical DAG-CBOR bytes for an operation object.
export function encodeOp (op) {
  return dagCbor.encode(op)
}

// Compute the did:plc for a signed genesis operation.
export function didForCreateOp (signedOp, truncate = 24) {
  const bytes = encodeOp(signedOp)
  const hash = sha256(bytes)
  const b32 = rawBase32(hash)
  return `did:plc:${b32.slice(0, truncate)}`
}

// The unsigned operation is everything except `sig`. did:plc signs the DAG-CBOR of
// the unsigned op; the signature is then attached and the DID hashes the signed form.
export function unsignedOp (op) {
  const { sig, ...rest } = op // eslint-disable-line no-unused-vars
  return rest
}

// The CID of a SIGNED operation — CIDv1, dag-cbor codec (0x71), sha2-256 multihash
// (0x12), rendered as a base32 string. This is the value the NEXT operation carries in
// its `prev` field to chain the PLC op-log (genesis has `prev: null`). Same encoding as
// atproto-repo/lib/cid.js cidForBytesSync, over DAG-CBOR(signedOp). Reference: the
// did-method-plc CID = CID.create(1, 0x71, sha256(cborEncode(op))).
export function opCid (signedOp) {
  const bytes = encodeOp(signedOp)
  const digest = Digest.create(0x12, sha256(bytes))
  return CID.createV1(dagCbor.code, digest).toString()
}