From 3b0c561d56617c61290afa9b8267b56a7b1c8a45 Mon Sep 17 00:00:00 2001 From: b5 Date: Tue, 8 Sep 2020 21:31:53 -0400 Subject: [PATCH] feat(didkey): didkey package properly implements v0.7 of spec --- .gitignore | 1 + didkey/key.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++ didkey/key_test.go | 28 +++++++++++++ example_test.go | 2 +- go.mod | 2 + token.go | 40 ++++++++++--------- token_test.go | 2 +- 7 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 .gitignore create mode 100644 didkey/key.go create mode 100644 didkey/key_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36946b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.txt \ No newline at end of file diff --git a/didkey/key.go b/didkey/key.go new file mode 100644 index 0000000..6291894 --- /dev/null +++ b/didkey/key.go @@ -0,0 +1,99 @@ +// Package didkey implements the did:key method. A DID Method for Static +// Cryptographic Keys. From the w3c draft spec +// https://w3c-ccg.github.io/did-method-key/ +package didkey + +import ( + "fmt" + "strings" + + "github.com/libp2p/go-libp2p-core/crypto" + mb "github.com/multiformats/go-multibase" + varint "github.com/multiformats/go-varint" +) + +const ( + // KeyPrefix indicates a decentralized identifier that uses the key method + KeyPrefix = "did:key" + // MulticodecKindRSAPubKey IS NOT A REAL MULTICODEC PREFIX. + // pulled from: https://github.com/w3c-ccg/lds-ed25519-2018/issues/3 because + // it's only slighly better than picking a random available byte prefix + MulticodecKindRSAPubKey = 0x5d + // MulticodecKindEd25519PubKey ed25519-pub + MulticodecKindEd25519PubKey = 0xed +) + +// ID is a DID:key identifier +type ID struct { + crypto.PubKey +} + +// MulticodecType indicates the type for this multicodec +func (id ID) MulticodecType() uint64 { + switch id.Type() { + case crypto.RSA: + return MulticodecKindRSAPubKey + case crypto.Ed25519: + return MulticodecKindEd25519PubKey + default: + panic("unexpected crypto type") + } +} + +// String returns this did:key formatted as a string +func (id ID) String() string { + raw, err := id.Raw() + if err != nil { + return "" + } + + t := id.MulticodecType() + size := varint.UvarintSize(t) + data := make([]byte, size+len(raw)) + n := varint.PutUvarint(data, t) + copy(data[n:], raw) + + b58BKeyStr, err := mb.Encode(mb.Base58BTC, data) + if err != nil { + return "" + } + + return fmt.Sprintf("%s:%s", KeyPrefix, b58BKeyStr) +} + +// Parse turns a string into a key method ID +func Parse(keystr string) (ID, error) { + var id ID + if !strings.HasPrefix(keystr, KeyPrefix) { + return id, fmt.Errorf("decentralized identifier is not a 'key' type") + } + + keystr = strings.TrimPrefix(keystr, KeyPrefix+":") + + _, data, err := mb.Decode(keystr) + if err != nil { + return id, err + } + + keyType, n, err := varint.FromUvarint(data) + if err != nil { + return id, err + } + + switch keyType { + case MulticodecKindRSAPubKey: + pub, err := crypto.UnmarshalRsaPublicKey(data[n:]) + if err != nil { + return id, err + } + return ID{pub}, nil + case MulticodecKindEd25519PubKey: + pub, err := crypto.UnmarshalEd25519PublicKey(data[n:]) + if err != nil { + return id, err + } + return ID{pub}, nil + } + + return id, fmt.Errorf("unrecognized key type multicodec prefix: %x", data[0]) +} diff --git a/didkey/key_test.go b/didkey/key_test.go new file mode 100644 index 0000000..2322fe4 --- /dev/null +++ b/didkey/key_test.go @@ -0,0 +1,28 @@ +package didkey + +import ( + "testing" +) + +func TestID(t *testing.T) { + keyStrED := "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" + id, err := Parse(keyStrED) + if err != nil { + t.Fatal(err) + } + + if id.String() != keyStrED { + t.Errorf("string mismatch.\nwant: %q\ngot: %q", keyStrED, id.String()) + } + + keyStrRSA := "did:key:zDQw3CC91XtvcBm5kY3FGBFgJarujMEKcarucbThDbGWVo3cAQPgvXuH5g9kxe9PNVHhAQpRQzTZQ3Pchra5pqKt6SgwTdgHXJtcWwp2pqkAmuNqhUj22naAg526RvU8u5ZE3tidrxh8zajWsrXriFwkVtZfTbEBGKewUMRVhttA3hc8Rpa6gL5HRqZ44Uq1fdKzdsBMTfG5ohhRzesiwoEtRyb3w4SPhdVYrD5Rd2KJPyTUqCvuuFwLAJJcDaPkp36RnRnFbbtZEydPtdMscnJTmj5nD9kpSmkTHU2riMVLcGnJWRHkHWZPnfbtuXcQVEwiBSjpJVRcCFG6LDbgTqt4s6BYxJqT5zriEaigzvFuj9V4s7QSJCs1Sj92tgDmE6YyTSR7UN9di2oHDCzk" + + id, err = Parse(keyStrRSA) + if err != nil { + t.Fatal(err) + } + + if id.String() != keyStrRSA { + t.Errorf("string mismatch.\nwant: %q\ngot: %q", keyStrRSA, id.String()) + } +} diff --git a/example_test.go b/example_test.go index fde3999..69e4bdd 100644 --- a/example_test.go +++ b/example_test.go @@ -9,7 +9,7 @@ import ( ) func ExampleWalkthrough() { - source, err := ucan.NewPrivKeyUCANSource(keyOne) + source, err := ucan.NewPrivKeySource(keyOne) panicIfError(err) subjectDID, err := ucan.DIDStringFromPublicKey(keyOne.GetPublic()) diff --git a/go.mod b/go.mod index c2f0c23..b770346 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,9 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/ipfs/go-cid v0.0.7 github.com/libp2p/go-libp2p-core v0.7.0 + github.com/multiformats/go-multibase v0.0.3 github.com/multiformats/go-multihash v0.0.14 + github.com/multiformats/go-varint v0.0.6 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect ) diff --git a/token.go b/token.go index b221c20..ec5bdb4 100644 --- a/token.go +++ b/token.go @@ -99,7 +99,6 @@ type Fact struct { // func (fct *Fact) UnmarshalJSON(p []byte) error { // var str string // if json.Unmarshal(p, &str); err == nil { - // } // } @@ -110,39 +109,42 @@ type CIDBytesResolver interface { ResolveCIDBytes(ctx context.Context, id cid.Cid) ([]byte, error) } -// UCANSource creates tokens, and provides a verification key for all tokens -// it creates +// Source creates tokens, and provides a verification key for all tokens it +// creates // -// implementations of UCANSource must conform to the assertion test defined -// in the spec subpackage -type UCANSource interface { +// implementations of Source must conform to the assertion test defined in the +// spec subpackage +type Source interface { NewOriginUCAN(subjectDID string, att Attenuations, fct []Fact, notBefore, expires time.Time) (*UCAN, error) NewAttenuatedUCAN(parent *UCAN, subjectDID string, att Attenuations, fct []Fact, notBefore, expires time.Time) (*UCAN, error) } -type pkUCANSource struct { +type pkSource struct { pk crypto.PrivKey issuerDID string signingMethod jwt.SigningMethod - verifyKey *rsa.PublicKey - signKey *rsa.PrivateKey + + verifyKey *rsa.PublicKey + signKey *rsa.PrivateKey ap AttenuationConstructor resolver CIDBytesResolver store TokenStore } -// assert pkUCANSource implements tokens at compile time -var _ UCANSource = (*pkUCANSource)(nil) +// assert pkSource implements tokens at compile time +var _ Source = (*pkSource)(nil) -// NewPrivKeyUCANSource creates an authentication interface backed by a single +// NewPrivKeySource creates an authentication interface backed by a single // private key. Intended for a node running as remote, or providing a public API -func NewPrivKeyUCANSource(privKey crypto.PrivKey) (UCANSource, error) { +func NewPrivKeySource(privKey crypto.PrivKey) (Source, error) { methodStr := "" - keyType := privKey.Type().String() + keyType := privKey.Type() switch keyType { - case "RSA": + case crypto.RSA: methodStr = "RS256" + case crypto.Ed25519: + methodStr = "EdDSA" default: return nil, fmt.Errorf("unsupported key type for token creation: %q", keyType) } @@ -176,7 +178,7 @@ func NewPrivKeyUCANSource(privKey crypto.PrivKey) (UCANSource, error) { return nil, err } - return &pkUCANSource{ + return &pkSource{ pk: privKey, signingMethod: signingMethod, verifyKey: verifyKey, @@ -185,11 +187,11 @@ func NewPrivKeyUCANSource(privKey crypto.PrivKey) (UCANSource, error) { }, nil } -func (a *pkUCANSource) NewOriginUCAN(subjectDID string, att Attenuations, fct []Fact, nbf, exp time.Time) (*UCAN, error) { +func (a *pkSource) NewOriginUCAN(subjectDID string, att Attenuations, fct []Fact, nbf, exp time.Time) (*UCAN, error) { return a.newUCAN(subjectDID, nil, att, fct, nbf, exp) } -func (a *pkUCANSource) NewAttenuatedUCAN(parent *UCAN, subjectDID string, att Attenuations, fct []Fact, nbf, exp time.Time) (*UCAN, error) { +func (a *pkSource) NewAttenuatedUCAN(parent *UCAN, subjectDID string, att Attenuations, fct []Fact, nbf, exp time.Time) (*UCAN, error) { if !parent.Attenuations.Contains(att) { return nil, fmt.Errorf("scope of ucan attenuations must be less than it's parent") } @@ -197,7 +199,7 @@ func (a *pkUCANSource) NewAttenuatedUCAN(parent *UCAN, subjectDID string, att At } // CreateToken returns a new JWT token -func (a *pkUCANSource) newUCAN(subjectDID string, prf []Proof, att Attenuations, fct []Fact, nbf, exp time.Time) (*UCAN, error) { +func (a *pkSource) newUCAN(subjectDID string, prf []Proof, att Attenuations, fct []Fact, nbf, exp time.Time) (*UCAN, error) { // create a signer for rsa 256 t := jwt.New(a.signingMethod) diff --git a/token_test.go b/token_test.go index 05a5c87..fe27024 100644 --- a/token_test.go +++ b/token_test.go @@ -40,7 +40,7 @@ func init() { } func TestPrivKeySource(t *testing.T) { - source, err := ucan.NewPrivKeyUCANSource(keyOne) + source, err := ucan.NewPrivKeySource(keyOne) if err != nil { t.Fatal(err) }