diff --git a/did/crypto.go b/did/crypto.go index 172065d..c79d5ef 100644 --- a/did/crypto.go +++ b/did/crypto.go @@ -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 } diff --git a/did/crypto_test.go b/did/crypto_test.go index 64723bd..6ee6fcb 100644 --- a/did/crypto_test.go +++ b/did/crypto_test.go @@ -5,6 +5,7 @@ import ( "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did" ) diff --git a/did/did.go b/did/did.go index 68f7219..9711178 100644 --- a/did/did.go +++ b/did/did.go @@ -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:" -func (d DID) Code() uint64 { - return d.code -} - -func (d DID) DID() DID { - return d -} - -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()) + if !strings.HasPrefix(str, keyPrefix) { + return Undef, fmt.Errorf("must start with 'did:key'") } - 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) + baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):]) + if err != nil { + return Undef, err } - - 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 + if baseCodec != mbase.Base58BTC { + return Undef, fmt.Errorf("not Base58BTC encoded") } - 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 +} diff --git a/did/did_test.go b/did/did_test.go index 6ee0b81..f536c32 100644 --- a/did/did_test.go +++ b/did/did_test.go @@ -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) }