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) 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(...).
- 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.
Summary
createDidDocumentemits verification methods withtype: "Multikey", but thepublicKeyMultibaseit puts on them is a multibase encoding of the raw public key bytes. A Multikey value has to bemultibase(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.createDidKeyUriin 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:publicKeyMultibaseemittedcreateDidKeyUri()on the same keyz8myPWEuZj3T3WzBQu281AeLzE3pmU9h7em1YEGiD6ickdid:key:z6MknEES6VA14awWdV27ab5r1jtz3d6ct2wULmvU4YgE1wQ8zMi3oP598f15BeBYp3tdwtNEE3omWSogmoT8Q5ckb1N5AKKJj59KU61vMYYncCBDzGDaDkn7zib2ACobFPXB8TjGTdid:key:zQ3shNCcRrVT3tm43o6JNjSjQaiBXvSb8kHtFhoNGR8eimFZsBoth columns encode the same key, so the multibase value on the verification method should equal the
did:keymethod-specific identifier. Today it does not.Two things differ:
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(...).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
createDidDocumentdocblock already shows the prefixed form as the expected shape:z6Mk…is the Ed25519 Multikey form. The code producesz8my…for an Ed25519 key.Why it matters
type: "Multikey"is the Data Integrity Multikey type, and itspublicKeyMultibaseis defined as a multicodec-prefixed value. A relying party that resolves one of thesedid:webdocuments 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
multibaseencoding:hexandbase58are converted to aMultikeyverification method too, and the repo's own tests exercise that (did-resolver.test.ts,resolve-did.test.ts,did-web.test.tsall passencoding: "hex"). Onlyencoding: "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
publicKeyMultibaseequal thedid:keyidentifier's multibase value for the same key, which is also a clean invariant to test.The prefixes live in
KEY_CONFIGinpackages/did/src/methods/did-key.tstoday, while the encoder that needs them isencodePublicKeyMultibaseinpackages/keys, which currently has no curve→multicodec mapping. So there is a placement decision here, and it changes thepublicKeyMultibaseevery 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/keysalongside the other curve metadata. Happy to move it, narrow it to thedidpackage, 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.