Skip to content

bug(did): publicKeyMultibase on Multikey verification methods is not a Multikey #165

Description

@Dusk1e

Summary

createDidDocument emits verification methods with type: "Multikey", but the publicKeyMultibase it puts on them is a multibase encoding of the raw public key bytes. A Multikey value has to be multibase(base58-btc, multicodec-prefix ‖ key-bytes), so the value carries no algorithm identifier and a consumer cannot tell an Ed25519 key from a P-256 one.

createDidKeyUri in this same package builds the prefixed form correctly, so the two disagree about how the same key is encoded.

Reproduction

Using the two keypairs already fixtured in packages/did/src/methods/did-key.test.ts:

const keypair = jwkToKeypair(jwk)

const doc = createDidDocumentFromKeypair({
  did: "did:web:example.com",
  keypair,
  encoding: "multibase", // "hex" and "base58" produce the same value
})

doc.verificationMethod[0].type // "Multikey"
doc.verificationMethod[0].publicKeyMultibase
createDidKeyUri(keypair)
curve publicKeyMultibase emitted createDidKeyUri() on the same key
Ed25519 z8myPWEuZj3T3WzBQu281AeLzE3pmU9h7em1YEGiD6ick did:key:z6MknEES6VA14awWdV27ab5r1jtz3d6ct2wULmvU4YgE1wQ8
secp256k1 zMi3oP598f15BeBYp3tdwtNEE3omWSogmoT8Q5ckb1N5AKKJj59KU61vMYYncCBDzGDaDkn7zib2ACobFPXB8TjGT did:key:zQ3shNCcRrVT3tm43o6JNjSjQaiBXvSb8kHtFhoNGR8eimFZs

Both columns encode the same key, so the multibase value on the verification method should equal the did:key method-specific identifier. Today it does not.

Two things differ:

  1. No multicodec prefix. encodePublicKeyMultibase (packages/keys/src/public-key.ts) takes curve and ignores it, calling bytesToMultibase(publicKey) on the raw bytes. createDidKeyUri prepends varint.encode(KEY_CONFIG[curve].multicodecPrefix) before encoding. The same drop happens in convertLegacyPublicKeyToMultibase (packages/did/src/create-did-document.ts), which converts hex and base58 inputs with a bare bytesToMultibase(...).
  2. Uncompressed EC keys. generateKeypair stores secp256k1/secp256r1 public keys uncompressed (65 bytes), which is why the secp256k1 row above is so long. secp256k1-pub (0xe7) and p256-pub (0x1200) identify the 33-byte compressed form — createDidKeyUri asks for compressed bytes and length-checks them against KEY_CONFIG.

The createDidDocument docblock already shows the prefixed form as the expected shape:

verificationMethod: {
  // ...
  publicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}

z6Mk… is the Ed25519 Multikey form. The code produces z8my… for an Ed25519 key.

Why it matters

type: "Multikey" is the Data Integrity Multikey type, and its publicKeyMultibase is defined as a multicodec-prefixed value. A relying party that resolves one of these did:web documents and follows that definition reads the leading bytes as an algorithm identifier and gets a wrong answer, or rejects the key.

This is reachable through the normal path, not just the multibase encoding: hex and base58 are converted to a Multikey verification method too, and the repo's own tests exercise that (did-resolver.test.ts, resolve-did.test.ts, did-web.test.ts all pass encoding: "hex"). Only encoding: "jwk", the default, is unaffected.

Proposed fix

Make the multibase encoding of a public key produce a Multikey: compress secp256k1/secp256r1 keys, prepend the curve's multicodec prefix, then base58-btc multibase encode. That makes publicKeyMultibase equal the did:key identifier's multibase value for the same key, which is also a clean invariant to test.

The prefixes live in KEY_CONFIG in packages/did/src/methods/did-key.ts today, while the encoder that needs them is encodePublicKeyMultibase in packages/keys, which currently has no curve→multicodec mapping. So there is a placement decision here, and it changes the publicKeyMultibase every existing caller emits.

I have a working change and will open it as a draft PR against this issue, taking the route that keeps the mapping in @agentcommercekit/keys alongside the other curve metadata. Happy to move it, narrow it to the did package, or drop it entirely if you would rather handle the placement or the compatibility question differently — the measurements above are the part I wanted to get in front of you.

AI assistance disclosure

Per the repository AI policy: this report was AI-assisted using Claude Code (Claude Opus). AI assistance was used to locate the inconsistency, produce the measurements above, and draft this issue. I reviewed the findings, can explain them, and take responsibility for what is reported here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions