diff --git a/delegation/delegation.go b/delegation/delegation.go index b89560d..e0bbefa 100644 --- a/delegation/delegation.go +++ b/delegation/delegation.go @@ -6,11 +6,13 @@ import ( "fmt" "time" + "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/crypto" "github.com/ucan-wg/go-ucan/capability/command" "github.com/ucan-wg/go-ucan/capability/policy" "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/internal/envelope" "github.com/ucan-wg/go-ucan/pkg/meta" ) @@ -33,6 +35,8 @@ type Token struct { notBefore *time.Time // The timestamp at which the Invocation becomes invalid expiration *time.Time + // The CID of the Token when enclosed in an Envelope and encoded to DAG-CBOR + cid cid.Cid } // New creates a validated Token from the provided parameters and options. @@ -69,6 +73,18 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po return nil, err } + cbor, err := tkn.ToDagCbor(privKey) + if err != nil { + return nil, err + } + + id, err := envelope.CIDFromBytes(cbor) + if err != nil { + return nil, err + } + + tkn.cid = id + return tkn, nil } @@ -132,6 +148,12 @@ func (t *Token) Expiration() *time.Time { return t.expiration } +// CID returns the content identifier of the Token model when enclosed +// in an Envelope and encoded to DAG-CBOR. +func (t *Token) CID() cid.Cid { + return t.cid +} + func (t *Token) validate() error { var errs error diff --git a/delegation/delegation_test.go b/delegation/delegation_test.go index 27e8954..9a2a6ad 100644 --- a/delegation/delegation_test.go +++ b/delegation/delegation_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gotest.tools/v3/golden" @@ -13,6 +14,7 @@ import ( "github.com/ucan-wg/go-ucan/capability/policy" "github.com/ucan-wg/go-ucan/delegation" "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/internal/envelope" ) const ( @@ -64,6 +66,9 @@ const ( ] ] ` + + newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e" + rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X" ) func TestConstructors(t *testing.T) { @@ -101,6 +106,7 @@ func TestConstructors(t *testing.T) { t.Log(string(data)) golden.Assert(t, string(data), "new.dagjson") + assert.Equal(t, newCID, envelope.CIDToBase58BTC(dlg.CID())) }) t.Run("Root", func(t *testing.T) { @@ -120,6 +126,7 @@ func TestConstructors(t *testing.T) { t.Log(string(data)) golden.Assert(t, string(data), "root.dagjson") + assert.Equal(t, rootCID, envelope.CIDToBase58BTC(dlg.CID())) }) } diff --git a/delegation/ipld.go b/delegation/ipld.go index a900238..c2620bd 100644 --- a/delegation/ipld.go +++ b/delegation/ipld.go @@ -3,6 +3,7 @@ package delegation import ( "io" + "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec" "github.com/ipld/go-ipld-prime/codec/dagcbor" @@ -100,19 +101,19 @@ func (t *Token) ToIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { // // An error is returned if the conversion fails, or if the resulting // View is invalid. -func Decode(b []byte, decFn codec.Decoder) (*Token, error) { +func Decode(b []byte, decFn codec.Decoder) (*Token, cid.Cid, error) { node, err := ipld.Decode(b, decFn) if err != nil { - return nil, err + return nil, cid.Undef, err } return FromIPLD(node) } // DecodeReader is the same as Decode, but accept an io.Reader. -func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) { +func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, cid.Cid, error) { node, err := ipld.DecodeStreaming(r, decFn) if err != nil { - return nil, err + return nil, cid.Undef, err } return FromIPLD(node) } @@ -121,12 +122,22 @@ func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) { // // An error is returned if the conversion fails, or if the resulting // View is invalid. -func FromDagCbor(data []byte) (*Token, error) { - return Decode(data, dagcbor.Decode) +func FromDagCbor(data []byte) (*Token, cid.Cid, error) { + pay, id, err := envelope.FromDagCbor[*tokenPayloadModel](data) + if err != nil { + return nil, cid.Undef, err + } + + tkn, err := tokenFromModel(*pay) + if err != nil { + return nil, cid.Undef, err + } + + return tkn, id, err } // FromDagCborReader is the same as FromDagCbor, but accept an io.Reader. -func FromDagCborReader(r io.Reader) (*Token, error) { +func FromDagCborReader(r io.Reader) (*Token, cid.Cid, error) { return DecodeReader(r, dagcbor.Decode) } @@ -134,12 +145,12 @@ func FromDagCborReader(r io.Reader) (*Token, error) { // // An error is returned if the conversion fails, or if the resulting // View is invalid. -func FromDagJson(data []byte) (*Token, error) { +func FromDagJson(data []byte) (*Token, cid.Cid, error) { return Decode(data, dagjson.Decode) } // FromDagJsonReader is the same as FromDagJson, but accept an io.Reader. -func FromDagJsonReader(r io.Reader) (*Token, error) { +func FromDagJsonReader(r io.Reader) (*Token, cid.Cid, error) { return DecodeReader(r, dagjson.Decode) } @@ -147,11 +158,16 @@ func FromDagJsonReader(r io.Reader) (*Token, error) { // // An error is returned if the conversion fails, or if the resulting // View is invalid. -func FromIPLD(node datamodel.Node) (*Token, error) { - tkn, _, err := envelope.FromIPLD[*tokenPayloadModel](node) // TODO add CID to view +func FromIPLD(node datamodel.Node) (*Token, cid.Cid, error) { + pay, id, err := envelope.FromIPLD[*tokenPayloadModel](node) // TODO add CID to view if err != nil { - return nil, err + return nil, cid.Undef, err } - return tokenFromModel(*tkn) + tkn, err := tokenFromModel(*pay) + if err != nil { + return nil, cid.Undef, err + } + + return tkn, id, err } diff --git a/delegation/schema_test.go b/delegation/schema_test.go index 0f534df..f63a968 100644 --- a/delegation/schema_test.go +++ b/delegation/schema_test.go @@ -10,43 +10,14 @@ import ( "gotest.tools/v3/golden" "github.com/ucan-wg/go-ucan/delegation" + "github.com/ucan-wg/go-ucan/internal/envelope" ) //go:embed delegation.ipldsch var schemaBytes []byte func TestSchemaRoundTrip(t *testing.T) { - // const delegationJson = ` - // { - // "aud":"did:key:def456", - // "cmd":"/foo/bar", - // "exp":123456, - // "iss":"did:key:abc123", - // "meta":{ - // "bar":"baaar", - // "foo":"fooo" - // }, - // "nbf":123456, - // "nonce":{ - // "/":{ - // "bytes":"c3VwZXItcmFuZG9t" - // } - // }, - // "pol":[ - // ["==", ".status", "draft"], - // ["all", ".reviewer", [ - // ["like", ".email", "*@example.com"]] - // ], - // ["any", ".tags", [ - // ["or", [ - // ["==", ".", "news"], - // ["==", ".", "press"]] - // ]] - // ] - // ], - // "sub":"" - // } - // ` + t.Parallel() delegationJson := golden.Get(t, "new.dagjson") privKey := privKey(t, issuerPrivKeyCfg) @@ -54,20 +25,22 @@ func TestSchemaRoundTrip(t *testing.T) { // format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson // function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson() - p1, err := delegation.FromDagJson([]byte(delegationJson)) + p1, id1, err := delegation.FromDagJson([]byte(delegationJson)) require.NoError(t, err) + require.Equal(t, newCID, envelope.CIDToBase58BTC(id1)) cborBytes, err := p1.ToDagCbor(privKey) require.NoError(t, err) fmt.Println("cborBytes length", len(cborBytes)) fmt.Println("cbor", string(cborBytes)) - p2, err := delegation.FromDagCbor(cborBytes) + p2, id2, err := delegation.FromDagCbor(cborBytes) require.NoError(t, err) fmt.Println("read Cbor", p2) readJson, err := p2.ToDagJson(privKey) require.NoError(t, err) + require.Equal(t, newCID, envelope.CIDToBase58BTC(id2)) fmt.Println("readJson length", len(readJson)) fmt.Println("json: ", string(readJson)) diff --git a/internal/envelope/cid.go b/internal/envelope/cid.go new file mode 100644 index 0000000..169d7c7 --- /dev/null +++ b/internal/envelope/cid.go @@ -0,0 +1,49 @@ +package envelope + +import ( + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" +) + +var b58BTCEnc = multibase.MustNewEncoder(multibase.Base58BTC) + +// CIDToBase56BTC is a utility method to convert a CIDv1 to the canonical +// string representation used by UCAN. +func CIDToBase58BTC(id cid.Cid) string { + return id.Encode(b58BTCEnc) +} + +// CID returns the UCAN content identifier a Tokener. +func CID(privKey crypto.PrivKey, token Tokener) (cid.Cid, error) { + data, err := ToDagCbor(privKey, token) + if err != nil { + return cid.Undef, err + } + + return CIDFromBytes(data) +} + +// CIDFromBytes returns the UCAN content identifier for an arbitrary slice +// of bytes. +func CIDFromBytes(b []byte) (cid.Cid, error) { + return cid.V1Builder{ + Codec: uint64(multicodec.DagCbor), + MhType: multihash.SHA2_256, + MhLength: 0, + }.Sum(b) +} + +// CIDFromIPLD returns the UCAN content identifier for an ipld.Node. +func CIDFromIPLD(node ipld.Node) (cid.Cid, error) { + data, err := ipld.Encode(node, dagcbor.Encode) + if err != nil { + return cid.Undef, nil + } + + return CIDFromBytes(data) +} diff --git a/internal/envelope/cid_test.go b/internal/envelope/cid_test.go new file mode 100644 index 0000000..3ff9d13 --- /dev/null +++ b/internal/envelope/cid_test.go @@ -0,0 +1,25 @@ +package envelope_test + +import ( + "testing" + + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/internal/envelope" + "gotest.tools/v3/golden" +) + +func TestCid(t *testing.T) { + t.Parallel() + + expData := golden.Get(t, "example.dagcbor") + expHash, err := multihash.Sum(expData, uint64(multicodec.Sha2_256), -1) + require.NoError(t, err) + + id, err := envelope.CID(examplePrivKey(t), newExample(t)) + require.NoError(t, err) + assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id)) + assert.Equal(t, expHash, id.Hash()) +} diff --git a/internal/envelope/example_test.go b/internal/envelope/example_test.go index d5a7ba8..f8fa05a 100644 --- a/internal/envelope/example_test.go +++ b/internal/envelope/example_test.go @@ -21,6 +21,7 @@ import ( ) const ( + exampleCID = "zdpuAyw6R5HvKSPzztuzXNYFx3ZGoMHMuAsXL6u3xLGQriRXQ" exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh" exampleGreeting = "world" examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4=" diff --git a/internal/envelope/ipld.go b/internal/envelope/ipld.go index d12b1be..6aa3a2d 100644 --- a/internal/envelope/ipld.go +++ b/internal/envelope/ipld.go @@ -90,7 +90,24 @@ func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, cid.Cid, erro // An error is returned if the conversion fails, or if the resulting // Tokener is invalid. func FromDagCbor[T Tokener](b []byte) (T, cid.Cid, error) { - return Decode[T](b, dagcbor.Decode) + undef := *new(T) + + node, err := ipld.Decode(b, dagcbor.Decode) + if err != nil { + return undef, cid.Undef, err + } + + id, err := CIDFromBytes(b) + if err != nil { + return undef, cid.Undef, err + } + + tkn, err := fromIPLD[T](node) + if err != nil { + return undef, cid.Undef, err + } + + return tkn, id, nil } // FromDagCborReader is the same as FromDagCbor, but accept an io.Reader. @@ -118,29 +135,45 @@ func FromDagJsonReader[T Tokener](r io.Reader) (T, cid.Cid, error) { func FromIPLD[T Tokener](node datamodel.Node) (T, cid.Cid, error) { undef := *new(T) - signatureNode, err := node.LookupByIndex(0) + id, err := CIDFromIPLD(node) if err != nil { return undef, cid.Undef, err } + tkn, err := fromIPLD[T](node) + if err != nil { + return undef, cid.Undef, err + } + + return tkn, id, nil +} + +func fromIPLD[T Tokener](node datamodel.Node) (T, error) { + undef := *new(T) + + signatureNode, err := node.LookupByIndex(0) + if err != nil { + return undef, err + } + signature, err := signatureNode.AsBytes() if err != nil { - return undef, cid.Undef, err + return undef, err } sigPayloadNode, err := node.LookupByIndex(1) if err != nil { - return undef, cid.Undef, err + return undef, err } varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey) if err != nil { - return undef, cid.Undef, err + return undef, err } tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag()) if err != nil { - return undef, cid.Undef, err + return undef, err } // This needs to be done before converting this node to it's schema @@ -148,7 +181,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, cid.Cid, error) { // to use the wire name). issuerNode, err := tokenPayloadNode.LookupByString("iss") if err != nil { - return undef, cid.Undef, err + return undef, err } // ^^^ @@ -160,7 +193,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, cid.Cid, error) { err = nb.AssignNode(tokenPayloadNode) if err != nil { - return undef, cid.Undef, err + return undef, err } tokenPayloadNode = nb.Build() @@ -168,12 +201,12 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, cid.Cid, error) { tokenPayload := bindnode.Unwrap(tokenPayloadNode) if tokenPayload == nil { - return undef, cid.Undef, errors.New("failed to Unwrap the TokenPayload") + return undef, errors.New("failed to Unwrap the TokenPayload") } tkn, ok := tokenPayload.(T) if !ok { - return undef, cid.Undef, errors.New("failed to assert the TokenPayload type as *token.Token") + return undef, errors.New("failed to assert the TokenPayload type as *token.Token") } // Check that the issuer's DID contains a public key with a type that @@ -181,45 +214,45 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, cid.Cid, error) { // vvv issuer, err := issuerNode.AsString() if err != nil { - return undef, cid.Undef, err + return undef, err } issuerDID, err := did.Parse(issuer) if err != nil { - return undef, cid.Undef, err + return undef, err } issuerPubKey, err := issuerDID.PubKey() if err != nil { - return undef, cid.Undef, err + return undef, err } issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type()) if err != nil { - return undef, cid.Undef, err + return undef, err } varsigHeader, err := varsigHeaderNode.AsBytes() if err != nil { - return undef, cid.Undef, err + return undef, err } if string(varsigHeader) != string(issuerVarsigHeader) { - return undef, cid.Undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type") + return undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type") } data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode) if err != nil { - return undef, cid.Undef, err + return undef, err } ok, err = issuerPubKey.Verify(data, signature) if err != nil || !ok { - return undef, cid.Undef, errors.New("failed to verify the token's signature") + return undef, errors.New("failed to verify the token's signature") } // ^^^ - return tkn, cid.Undef, nil + return tkn, nil } // Encode marshals a Tokener to the format specified by the provided diff --git a/internal/envelope/ipld_test.go b/internal/envelope/ipld_test.go index 132106d..24001a8 100644 --- a/internal/envelope/ipld_test.go +++ b/internal/envelope/ipld_test.go @@ -59,35 +59,69 @@ func TestEncode(t *testing.T) { func TestRoundtrip(t *testing.T) { t.Parallel() + t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) { + t.Parallel() + + dataIn := golden.Get(t, exampleDAGCBORFilename) + + tkn, id, err := envelope.FromDagCbor[*Example](dataIn) + require.NoError(t, err) + assert.Equal(t, exampleGreeting, tkn.Hello) + assert.Equal(t, exampleDID, tkn.Issuer) + assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id)) + + dataOut, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t)) + require.NoError(t, err) + assert.Equal(t, dataIn, dataOut) + }) + t.Run("via FromDagCborReader/ToDagCborWriter", func(t *testing.T) { t.Parallel() data := golden.Get(t, exampleDAGCBORFilename) - tkn, _, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data)) + tkn, id, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data)) require.NoError(t, err) assert.Equal(t, exampleGreeting, tkn.Hello) assert.Equal(t, exampleDID, tkn.Issuer) + assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id)) w := &bytes.Buffer{} require.NoError(t, envelope.ToDagCborWriter(w, examplePrivKey(t), newExample(t))) assert.Equal(t, data, w.Bytes()) }) - t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) { + t.Run("via FromDagJson/ToDagJson", func(t *testing.T) { t.Parallel() - dataIn := golden.Get(t, exampleDAGCBORFilename) + dataIn := golden.Get(t, exampleDAGJSONFilename) - tkn, _, err := envelope.FromDagCbor[*Example](dataIn) + tkn, id, err := envelope.FromDagJson[*Example](dataIn) require.NoError(t, err) assert.Equal(t, exampleGreeting, tkn.Hello) assert.Equal(t, exampleDID, tkn.Issuer) + assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id)) - dataOut, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t)) + dataOut, err := envelope.ToDagJson(examplePrivKey(t), newExample(t)) require.NoError(t, err) assert.Equal(t, dataIn, dataOut) }) + + t.Run("via FromDagJsonReader/ToDagJsonrWriter", func(t *testing.T) { + t.Parallel() + + data := golden.Get(t, exampleDAGJSONFilename) + + tkn, id, err := envelope.FromDagJsonReader[*Example](bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, exampleGreeting, tkn.Hello) + assert.Equal(t, exampleDID, tkn.Issuer) + assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id)) + + w := &bytes.Buffer{} + require.NoError(t, envelope.ToDagJsonWriter(w, examplePrivKey(t), newExample(t))) + assert.Equal(t, data, w.Bytes()) + }) } func TestFromIPLD_with_invalid_signature(t *testing.T) {