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:
id.Raw()— the raw public key bytes from libp2p (33 or 65 bytes for secp256k1, 32 for Ed25519, DER PKIX for RSA).- An unsigned-varint multicodec prefix identifying the key type is prepended.
- 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
NewDID?func(pub crypto.PubKey) (DID, error)
Wraps a libp2p public key. Accepts Ed25519, RSA, Secp256k1; errors on any other key type.
func(pub crypto.PubKey) (DID, error)NewFromPubKey?func(pub PubKey) DID
Wraps this package's own PubKey (a curves.Point-backed secp256k1 key). Infallible.
func(pub PubKey) DIDNewFromMPCPubKey?func(pubKeyBytes []byte) (DID, error)
Unmarshals 33- or 65-byte secp256k1 public key bytes straight from an MPC enclave. Errors on any other length.
func(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.
func(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.
func(didString string) errorThe DID type
DID embeds crypto.PubKey, so every libp2p method (Raw, Type, Equals, Verify, Bytes) is
promoted onto it. On top of that:
String?func() string
The did:key identifier. Returns "" — not an error — if Raw() or multibase encoding fails.
func() stringPublicKey?func() crypto.PubKey
The embedded libp2p public key.
func() crypto.PubKeyMulticodecType?func() uint64
The multicodec for this key type. PANICS on an unrecognised key type rather than returning an error.
func() uint64CompressedPubKey?func() ([]byte, error)
33-byte compressed point for secp256k1 (converting from 65 bytes if needed); raw bytes for every other key type.
func() ([]byte, error)VerifyKey?func() (any, error)
*rsa.PublicKey for RSA, ed25519.PublicKey for Ed25519, and the raw []byte for secp256k1.
func() (any, error)Address?func() (string, error)
A "sonr1"-prefixed string. See the caveat below — it is not a hash and not bech32.
func() (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.
Bytes?func() []byte
point.ToAffineCompressed() — 33 bytes on secp256k1.
func() []byteRaw?func() ([]byte, error)
Identical to Bytes; the error is always nil.
func() ([]byte, error)Hex?func() string
Hex of the compressed point.
func() stringType?func() p2ppb.KeyType
Hardcoded to KeyType_Secp256k1 regardless of the point's actual curve.
func() p2ppb.KeyTypeEquals?func(b p2pcrypto.Key) bool
Compares Raw() bytes.
func(b p2pcrypto.Key) boolVerify?func(msg, sig []byte) (bool, error)
ECDSA verify over a SHA3-256 digest. Signature layout below.
func(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:
- Requires the signature to be exactly 66 bytes, rejecting anything else with
"malformed signature: not the correct size". - Parses it as
V || R || S, whereVis a single recovery-id byte at offset 0,Rissig[1:33], andSissig[33:66]. - Hashes the message with SHA3-256 (not SHA-256) and calls
ecdsa.Verifyon that digest, ignoringVentirely. - Reconstructs the ECDSA public key by slicing the compressed point as
x = bytes[1:33],y = bytes[33:]oncurves.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. |