Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

did:key Identifiers

Encode a public key as a self-describing did:key string, parse it back, and derive verification material — plus a frank assessment of the keys/parsers package.

github.com/sonr-io/crypto/keys turns a public key into a stable, self-describing string and back again. A did:key identifier needs no registry and no network lookup: the key material is the identifier, so resolving one is a pure decode. The package wraps libp2p’s github.com/libp2p/go-libp2p/core/crypto.PubKey interface, which gives it RSA, Ed25519, and secp256k1 support for free, and adds a secp256k1-specific path for public keys that arrive as raw bytes from an MPC enclave.

Reach for this when you need a canonical identifier for a key you already hold — a UCAN issuer, a log line, a database column, a delegation audience.

Do not reach for this when you need a DID with mutable state (rotation, service endpoints, multiple verification methods). did:key is immutable by construction: change the key, change the identifier. The DIDMethod enum in this package names other methods, but only did:key is implemented here.

Encoding

DID.String() builds the identifier in three steps:

  1. id.Raw() — the raw public key bytes from libp2p (33 or 65 bytes for secp256k1, 32 for Ed25519, DER PKIX for RSA).
  2. An unsigned-varint multicodec prefix identifying the key type is prepended.
  3. The whole buffer is multibase-encoded with base58btc, which yields the leading z.

So every identifier this package produces looks like did:key:z…. Parse reverses exactly those steps and rejects any multibase encoding other than base58btc.

Key type Constant Multicodec Accepted raw lengths
RSA (rsa-x509-pub) MulticodecKindRSAPubKey 0x1205 DER, parsed via x509.ParsePKIXPublicKey
Ed25519 (ed25519-pub) MulticodecKindEd25519PubKey 0xed 32
secp256k1 (secp256k1-pub) MulticodecKindSecp256k1PubKey 0xe7 33 (compressed) or 65 (uncompressed)

KeyPrefix is the string constant "did:key". GetMulticodecType(keyType int) maps an int(crypto.RSA) / int(crypto.Ed25519) / int(crypto.Secp256k1) to the values above and errors on anything else.

Constructors

PropType
NewDID?func(pub crypto.PubKey) (DID, error)

Wraps a libp2p public key. Accepts Ed25519, RSA, Secp256k1; errors on any other key type.

Typefunc(pub crypto.PubKey) (DID, error)
NewFromPubKey?func(pub PubKey) DID

Wraps this package's own PubKey (a curves.Point-backed secp256k1 key). Infallible.

Typefunc(pub PubKey) DID
NewFromMPCPubKey?func(pubKeyBytes []byte) (DID, error)

Unmarshals 33- or 65-byte secp256k1 public key bytes straight from an MPC enclave. Errors on any other length.

Typefunc(pubKeyBytes []byte) (DID, error)
Parse?func(keystr string) (DID, error)

Decodes a did:key string. Requires the did:key prefix, base58btc multibase, and a recognised multicodec.

Typefunc(keystr string) (DID, error)
ValidateFormat?func(didString string) error

Prefix check followed by a full Parse. Use when you only need a yes/no on a string.

Typefunc(didString string) error

The DID type

DID embeds crypto.PubKey, so every libp2p method (Raw, Type, Equals, Verify, Bytes) is promoted onto it. On top of that:

PropType
String?func() string

The did:key identifier. Returns "" — not an error — if Raw() or multibase encoding fails.

Typefunc() string
PublicKey?func() crypto.PubKey

The embedded libp2p public key.

Typefunc() crypto.PubKey
MulticodecType?func() uint64

The multicodec for this key type. PANICS on an unrecognised key type rather than returning an error.

Typefunc() uint64
CompressedPubKey?func() ([]byte, error)

33-byte compressed point for secp256k1 (converting from 65 bytes if needed); raw bytes for every other key type.

Typefunc() ([]byte, error)
VerifyKey?func() (any, error)

*rsa.PublicKey for RSA, ed25519.PublicKey for Ed25519, and the raw []byte for secp256k1.

Typefunc() (any, error)
Address?func() (string, error)

A "sonr1"-prefixed string. See the caveat below — it is not a hash and not bech32.

Typefunc() (string, error)

Round trip

Grounded in TestDIDStringFormat and TestMPCIntegration in keys/didkey_test.go:

package main

import (
	"crypto/rand"
	"fmt"

	p2pcrypto "github.com/libp2p/go-libp2p/core/crypto"
	"github.com/sonr-io/crypto/keys"
)

func main() {
	priv, _, err := p2pcrypto.GenerateSecp256k1Key(rand.Reader)
	if err != nil {
		panic(err)
	}

	did, err := keys.NewDID(priv.GetPublic())
	if err != nil {
		panic(err)
	}

	s := did.String() // "did:key:z..."
	fmt.Println(s)

	parsed, err := keys.Parse(s)
	if err != nil {
		panic(err)
	}

	// The encoding is canonical for a given input: re-stringifying is identical.
	fmt.Println("stable:", parsed.String() == s)
	fmt.Println("same type:", parsed.Type() == did.Type())

	// Cheap validity check on an untrusted string.
	fmt.Println("valid:", keys.ValidateFormat(s) == nil)

	compressed, err := parsed.CompressedPubKey()
	fmt.Println("compressed len:", len(compressed), err) // 33
}

For a key that arrives from an enclave rather than a libp2p keypair, swap the constructor:

did, err := keys.NewFromMPCPubKey(enclave.PubKeyBytes())

DIDMethod

A plain string enum, verbatim from keys/methods.go. It carries no behaviour beyond String(), and nothing else in the package consumes it — it exists for callers that need to tag which method a DID string belongs to.

const (
	DIDMethodKey      DIDMethod = "key"
	DIDMethodSonr     DIDMethod = "sonr"
	DIDMehthodBitcoin DIDMethod = "btcr"
	DIDMethodEthereum DIDMethod = "ethr"
	DIDMethodCbor     DIDMethod = "cbor"
	DIDMethodCID      DIDMethod = "cid"
	DIDMethodIPFS     DIDMethod = "ipfs"
)

The PubKey interface

Separate from libp2p’s type, keys.PubKey adapts a curves.Point into something DID can embed. NewPubKey(pk curves.Point) PubKey is the only constructor.

PropType
Bytes?func() []byte

point.ToAffineCompressed() — 33 bytes on secp256k1.

Typefunc() []byte
Raw?func() ([]byte, error)

Identical to Bytes; the error is always nil.

Typefunc() ([]byte, error)
Hex?func() string

Hex of the compressed point.

Typefunc() string
Type?func() p2ppb.KeyType

Hardcoded to KeyType_Secp256k1 regardless of the point's actual curve.

Typefunc() p2ppb.KeyType
Equals?func(b p2pcrypto.Key) bool

Compares Raw() bytes.

Typefunc(b p2pcrypto.Key) bool
Verify?func(msg, sig []byte) (bool, error)

ECDSA verify over a SHA3-256 digest. Signature layout below.

Typefunc(msg, sig []byte) (bool, error)

The 66-byte signature layout

PubKey.Verify does not accept a standard 64-byte r || s signature. Reading keys/pubkey.go and keys/utils.go, it:

  1. Requires the signature to be exactly 66 bytes, rejecting anything else with "malformed signature: not the correct size".
  2. Parses it as V || R || S, where V is a single recovery-id byte at offset 0, R is sig[1:33], and S is sig[33:66].
  3. Hashes the message with SHA3-256 (not SHA-256) and calls ecdsa.Verify on that digest, ignoring V entirely.
  4. Reconstructs the ECDSA public key by slicing the compressed point as x = bytes[1:33], y = bytes[33:] on curves.K256().

Address() does not do what its comment says

The doc comment promises “a blockchain-compatible address” and an inline comment claims “first 20 bytes of Keccak-256 hash (Ethereum-style)”. The code does neither:

// keys/didkey.go, secp256k1 branch, verbatim:
return fmt.Sprintf("sonr1%x", rawPubBytes[:8]), nil

Avoid keys/parsers

keys/parsers looks like a set of per-chain address parsers. It is not. Verified by reading every file in the directory:

File Lines Contents
btc_parser.go 1 package parsers
eth_parser.go 1 package parsers
fil_parser.go 1 package parsers
sol_parser.go 1 package parsers
ton_parser.go 1 package parsers
cosmos_parser.go 12 A CosmosPrefix string type and six bech32 HRP constants. No functions.
key_parser.go 157 A near-verbatim copy of keys/didkey.go, exporting DIDKey instead of DID.

Caveats

Next

Last updated on September 2, 2026

Was this page helpful?