Repository navigation
bug(did): publicKeyMultibase on Multikey verification methods is not a Multikey #165
Description
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:
- No multicodec prefix.
encodePublicKeyMultibase(packages/keys/src/public-key.ts) takescurveand ignores it, callingbytesToMultibase(publicKey)on the raw bytes.createDidKeyUriprependsvarint.encode(KEY_CONFIG[curve].multicodecPrefix)before encoding. The same drop happens inconvertLegacyPublicKeyToMultibase(packages/did/src/create-did-document.ts), which convertshexandbase58inputs with a barebytesToMultibase(...). - Uncompressed EC keys.
generateKeypairstores secp256k1/secp256r1 public keys uncompressed (65 bytes), which is why the secp256k1 row above is so long.secp256k1-pub(0xe7) andp256-pub(0x1200) identify the 33-byte compressed form —createDidKeyUriasks for compressed bytes and length-checks them againstKEY_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.
Activity
jfreyna commentedon Aug 24, 2026
Ran into this on 0.11.0 and reproduced it independently.
The package disagrees with itself. Same Ed25519 keypair, two code paths:
createDidKeyUriis correct. The value that ends up in aMultikeyverification method comes from the other path, sopublicKeyMultibaseis base58btc of the bare key rather than a Multikey.The downstream effect is that it does not round-trip inside this monorepo. A
did:webdocument built withencoding: "multibase"fails verification through@agentcommercekit/jwtwithinvalid_signature: no matching public key found. The same document withencoding: "jwk"verifies.Tested on Ed25519 only. #166 looks like the right fix.