did: simplify public API, add missing required algorithm

This commit is contained in:
Michael Muré
2024-10-14 15:16:38 +02:00
parent 59da2d1a2c
commit f8b5fa3a32
4 changed files with 104 additions and 156 deletions

View File

@@ -1,6 +1,7 @@
package did
import (
"crypto/rand"
"errors"
crypto "github.com/libp2p/go-libp2p/core/crypto"
@@ -9,6 +10,42 @@ import (
"github.com/multiformats/go-varint"
)
// GenerateEd25519 generates an Ed25519 private key and the matching DID.
// This is the RECOMMENDED algorithm.
func GenerateEd25519() (crypto.PrivKey, DID, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, Undef, nil
}
did, err := FromPubKey(pub)
return priv, did, err
}
// GenerateRSA generates a RSA private key and the matching DID.
func GenerateSecp256k1() (crypto.PrivKey, DID, error) {
// NIST Special Publication 800-57 Part 1 Revision 5
// Section 5.6.1.1 (Table 2)
// Paraphrased: 2048-bit RSA keys are secure until 2030 and 3072-bit keys are recommended for longer-term security.
const keyLength = 3072
priv, pub, err := crypto.GenerateRSAKeyPair(keyLength, rand.Reader)
if err != nil {
return nil, Undef, nil
}
did, err := FromPubKey(pub)
return priv, did, err
}
// GenerateEd25519 generates a Secp256k1 private key and the matching DID.
func GenerateRSA() (crypto.PrivKey, DID, error) {
priv, pub, err := crypto.GenerateSecp256k1Key(rand.Reader)
if err != nil {
return nil, Undef, nil
}
did, err := FromPubKey(pub)
return priv, did, err
}
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
return FromPubKey(privKey.GetPublic())
}
@@ -21,20 +58,17 @@ func FromPubKey(pubKey crypto.PubKey) (DID, error) {
pb.KeyType_ECDSA: multicodec.Es256,
}[pubKey.Type()]
if !ok {
return Undef, errors.New("Blah")
return Undef, errors.New("unsupported key type")
}
buf := varint.ToUvarint(uint64(code))
pubBytes, err := pubKey.Raw()
if err != nil {
return Undef, err
}
return DID{
str: string(append(buf, pubBytes...)),
code: uint64(code),
key: true,
code: code,
bytes: string(append(varint.ToUvarint(uint64(code)), pubBytes...)),
}, nil
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did"
)

View File

@@ -10,111 +10,46 @@ import (
varint "github.com/multiformats/go-varint"
)
const Prefix = "did:"
const KeyPrefix = "did:key:"
const DIDCore = 0x0d1d
const Ed25519 = 0xed
const RSA = uint64(multicodec.RsaPub)
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
//
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
type DID struct {
key bool
code uint64
str string
}
const Ed25519 = multicodec.Ed25519Pub // recommended
const P256 = multicodec.P256Pub
const Secp256k1 = multicodec.Secp256k1Pub
const RSA = multicodec.RsaPub
// Undef can be used to represent a nil or undefined DID, using DID{}
// directly is also acceptable.
var Undef = DID{}
func (d DID) Defined() bool {
return d.str != ""
// DID is a Decentralized Identifier of the did:key type, directly holding a cryptographic public key.
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
type DID struct {
code multicodec.Code
bytes string // as string instead of []byte to allow the == operator
}
func (d DID) Bytes() []byte {
if !d.Defined() {
return nil
}
return []byte(d.str)
func Parse(str string) (DID, error) {
const keyPrefix = "did:key:"
if !strings.HasPrefix(str, keyPrefix) {
return Undef, fmt.Errorf("must start with 'did:key'")
}
func (d DID) Code() uint64 {
return d.code
baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):])
if err != nil {
return Undef, err
}
func (d DID) DID() DID {
return d
if baseCodec != mbase.Base58BTC {
return Undef, fmt.Errorf("not Base58BTC encoded")
}
func (d DID) Key() bool {
return d.key
}
func (d DID) PubKey() (crypto.PubKey, error) {
if !d.key {
return nil, fmt.Errorf("unsupported did type: %s", d.String())
}
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
multicodec.Ed25519Pub: crypto.UnmarshalEd25519PublicKey,
multicodec.RsaPub: crypto.UnmarshalRsaPublicKey,
multicodec.Secp256k1Pub: crypto.UnmarshalSecp256k1PublicKey,
multicodec.Es256: crypto.UnmarshalECDSAPublicKey,
}[multicodec.Code(d.code)]
if !ok {
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
}
return unmarshaler(d.Bytes()[varint.UvarintSize(d.code):])
}
// String formats the decentralized identity document (DID) as a string.
func (d DID) String() string {
if d.key {
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.str))
return "did:key:" + key
}
return "did:" + d.str[MethodOffset:]
}
func Decode(bytes []byte) (DID, error) {
code, _, err := varint.FromUvarint(bytes)
if err != nil {
return Undef, err
}
if code == Ed25519 || code == RSA {
return DID{str: string(bytes), code: code, key: true}, nil
} else if code == DIDCore {
return DID{str: string(bytes)}, nil
switch multicodec.Code(code) {
case Ed25519, P256, Secp256k1, RSA:
return DID{bytes: string(bytes), code: multicodec.Code(code)}, nil
default:
return Undef, fmt.Errorf("unsupported did:key multicodec: 0x%x", code)
}
return Undef, fmt.Errorf("unsupported DID encoding: 0x%x", code)
}
func Parse(str string) (DID, error) {
if !strings.HasPrefix(str, Prefix) {
return Undef, fmt.Errorf("must start with 'did:'")
}
if strings.HasPrefix(str, KeyPrefix) {
code, bytes, err := mbase.Decode(str[len(KeyPrefix):])
if err != nil {
return Undef, err
}
if code != mbase.Base58BTC {
return Undef, fmt.Errorf("not Base58BTC encoded")
}
return Decode(bytes)
}
buf := make([]byte, MethodOffset)
varint.PutUvarint(buf, DIDCore)
suffix, _ := strings.CutPrefix(str, Prefix)
buf = append(buf, suffix...)
return DID{str: string(buf), code: DIDCore}, nil
}
func MustParse(str string) DID {
@@ -124,3 +59,29 @@ func MustParse(str string) DID {
}
return did
}
// Defined tells if the DID is defined, not equal to Undef.
func (d DID) Defined() bool {
return d.code == 0 || len(d.bytes) > 0
}
func (d DID) PubKey() (crypto.PubKey, error) {
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
Ed25519: crypto.UnmarshalEd25519PublicKey,
P256: crypto.UnmarshalECDSAPublicKey,
Secp256k1: crypto.UnmarshalSecp256k1PublicKey,
RSA: crypto.UnmarshalRsaPublicKey,
}[d.code]
if !ok {
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
}
codeSize := varint.UvarintSize(uint64(d.code))
return unmarshaler([]byte(d.bytes)[codeSize:])
}
// String formats the decentralized identity document (DID) as a string.
func (d DID) String() string {
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
return "did:key:" + key
}

View File

@@ -9,12 +9,8 @@ import (
func TestParseDIDKey(t *testing.T) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
d, err := Parse(str)
if err != nil {
t.Fatalf("%v", err)
}
if d.String() != str {
t.Fatalf("expected %v to equal %v", d.String(), str)
}
require.NoError(t, err)
require.Equal(t, str, d.String())
}
func TestMustParseDIDKey(t *testing.T) {
@@ -29,65 +25,21 @@ func TestMustParseDIDKey(t *testing.T) {
})
}
func TestDecodeDIDKey(t *testing.T) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
d0, err := Parse(str)
if err != nil {
t.Fatalf("%v", err)
}
d1, err := Decode(d0.Bytes())
if err != nil {
t.Fatalf("%v", err)
}
if d1.String() != str {
t.Fatalf("expected %v to equal %v", d1.String(), str)
}
}
func TestParseDIDWeb(t *testing.T) {
str := "did:web:up.web3.storage"
d, err := Parse(str)
if err != nil {
t.Fatalf("%v", err)
}
if d.String() != str {
t.Fatalf("expected %v to equal %v", d.String(), str)
}
}
func TestDecodeDIDWeb(t *testing.T) {
str := "did:web:up.web3.storage"
d0, err := Parse(str)
if err != nil {
t.Fatalf("%v", err)
}
d1, err := Decode(d0.Bytes())
if err != nil {
t.Fatalf("%v", err)
}
if d1.String() != str {
t.Fatalf("expected %v to equal %v", d1.String(), str)
}
func TestRoundTrip(t *testing.T) {
// TODO: round-trip pubkey-->did-->pubkey for all supported types
}
func TestEquivalence(t *testing.T) {
u0 := DID{}
u1 := Undef
if u0 != u1 {
t.Fatalf("undef DID not equivalent")
}
undef0 := DID{}
undef1 := Undef
d0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
if err != nil {
t.Fatalf("%v", err)
}
did0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
require.NoError(t, err)
did1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
require.NoError(t, err)
d1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
if err != nil {
t.Fatalf("%v", err)
}
if d0 != d1 {
t.Fatalf("two equivalent DID not equivalent")
}
require.True(t, undef0 == undef1)
require.False(t, undef0 == did0)
require.True(t, did0 == did1)
require.False(t, undef1 == did1)
}