rename tokens to token
This commit is contained in:
124
token/internal/envelope/cid.go
Normal file
124
token/internal/envelope/cid.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package envelope
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
var _ io.Reader = (*CIDReader)(nil)
|
||||
|
||||
// CIDReader wraps an io.Reader and includes a hash.Hash that is
|
||||
// incrementally updated as data is read from the child io.Reader.
|
||||
type CIDReader struct {
|
||||
hash hash.Hash
|
||||
r io.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
// NewCIDReader initializes a hash.Hash to calculate the CID's hash and
|
||||
// returns the wrapped io.Reader.
|
||||
func NewCIDReader(r io.Reader) *CIDReader {
|
||||
h := sha256.New()
|
||||
h.Reset()
|
||||
|
||||
return &CIDReader{
|
||||
hash: h,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
// CID returns the UCAN-formatted cid.Cid created from the hash calculated
|
||||
// as bytes were read from the inner io.Reader.
|
||||
func (r *CIDReader) CID() (cid.Cid, error) {
|
||||
if r.err != nil {
|
||||
return cid.Undef, r.err // TODO: Wrap to say it's an error during streaming?
|
||||
}
|
||||
|
||||
return cidFromHash(r.hash)
|
||||
}
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (r *CIDReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.r.Read(p)
|
||||
if err != nil && err != io.EOF {
|
||||
r.err = err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = r.hash.Write(p[:n])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var _ io.Writer = (*CIDWriter)(nil)
|
||||
|
||||
// CIDWriter wraps an io.Writer and includes a hash.Hash that is
|
||||
// incrementally updated as data is written to the child io.Writer.
|
||||
type CIDWriter struct {
|
||||
hash hash.Hash
|
||||
w io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
// NewCIDWriter initializes a hash.Hash to calculate the CID's hash and
|
||||
// returns the wrapped io.Writer.
|
||||
func NewCIDWriter(w io.Writer) *CIDWriter {
|
||||
h := sha256.New()
|
||||
h.Reset()
|
||||
|
||||
return &CIDWriter{
|
||||
hash: h,
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
// CID returns the UCAN-formatted cid.Cid created from the hash calculated
|
||||
// as bytes were written from the inner io.Reader.
|
||||
func (w *CIDWriter) CID() (cid.Cid, error) {
|
||||
return cidFromHash(w.hash)
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *CIDWriter) Write(p []byte) (n int, err error) {
|
||||
if _, err = w.hash.Write(p); err != nil {
|
||||
w.err = err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return w.w.Write(p)
|
||||
}
|
||||
|
||||
func cidFromHash(hash hash.Hash) (cid.Cid, error) {
|
||||
mh, err := multihash.Encode(hash.Sum(nil), multihash.SHA2_256)
|
||||
if err != nil {
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
return cid.NewCidV1(uint64(multicodec.DagCbor), mh), nil
|
||||
}
|
||||
86
token/internal/envelope/cid_test.go
Normal file
86
token/internal/envelope/cid_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package envelope_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
func TestCidFromBytes(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)
|
||||
|
||||
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id))
|
||||
assert.Equal(t, expHash, id.Hash())
|
||||
}
|
||||
|
||||
func TestStreaming(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expData := []byte("this is a test")
|
||||
|
||||
expCID, err := cid.V1Builder{
|
||||
Codec: uint64(multicodec.DagCbor),
|
||||
MhType: multihash.SHA2_256,
|
||||
MhLength: 0,
|
||||
}.Sum(expData)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("CIDReader()", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r, w := io.Pipe() //nolint:varnamelen
|
||||
cidReader := envelope.NewCIDReader(r)
|
||||
|
||||
go func() {
|
||||
_, err := w.Write(expData)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, w.Close())
|
||||
}()
|
||||
|
||||
actData, err := io.ReadAll(cidReader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expData, actData)
|
||||
|
||||
actCID, err := cidReader.CID()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expCID, actCID)
|
||||
})
|
||||
|
||||
t.Run("CIDWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r, w := io.Pipe() //nolint:varnamelen
|
||||
cidWriter := envelope.NewCIDWriter(w)
|
||||
|
||||
go func() {
|
||||
_, err := cidWriter.Write(expData)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, w.Close())
|
||||
}()
|
||||
|
||||
actData, err := io.ReadAll(r)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expData, actData)
|
||||
|
||||
actCID, err := cidWriter.CID()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expCID, actCID)
|
||||
})
|
||||
}
|
||||
138
token/internal/envelope/example_test.go
Normal file
138
token/internal/envelope/example_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package envelope_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
const (
|
||||
exampleCID = "zdpuAyw6R5HvKSPzztuzXNYFx3ZGoMHMuAsXL6u3xLGQriRXQ"
|
||||
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
|
||||
exampleGreeting = "world"
|
||||
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
|
||||
exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
|
||||
exampleTag = "ucan/example@v1.0.0-rc.1"
|
||||
exampleTypeName = "Example"
|
||||
exampleVarsigHeaderStr = "NO0BcQ"
|
||||
|
||||
invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK"
|
||||
|
||||
exampleDAGCBORFilename = "example.dagcbor"
|
||||
exampleDAGJSONFilename = "example.dagjson"
|
||||
)
|
||||
|
||||
//go:embed testdata/example.ipldsch
|
||||
var schemaBytes []byte
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
)
|
||||
|
||||
func mustLoadSchema() *schema.TypeSystem {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||
}
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
func exampleType() schema.Type {
|
||||
return mustLoadSchema().TypeByName(exampleTypeName)
|
||||
}
|
||||
|
||||
var _ envelope.Tokener = (*Example)(nil)
|
||||
|
||||
type Example struct {
|
||||
Hello string
|
||||
Issuer string
|
||||
}
|
||||
|
||||
func newExample(t *testing.T) *Example {
|
||||
t.Helper()
|
||||
|
||||
return &Example{
|
||||
Hello: exampleGreeting,
|
||||
Issuer: exampleDID,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Example) Prototype() schema.TypedPrototype {
|
||||
return bindnode.Prototype(e, exampleType())
|
||||
}
|
||||
|
||||
func (*Example) Tag() string {
|
||||
return exampleTag
|
||||
}
|
||||
|
||||
func exampleGoldenNode(t *testing.T) datamodel.Node {
|
||||
t.Helper()
|
||||
|
||||
cbor := golden.Get(t, exampleDAGCBORFilename)
|
||||
|
||||
node, err := ipld.Decode(cbor, dagcbor.Decode)
|
||||
require.NoError(t, err)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func examplePrivKey(t *testing.T) crypto.PrivKey {
|
||||
t.Helper()
|
||||
|
||||
privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc)
|
||||
require.NoError(t, err)
|
||||
|
||||
return privKey
|
||||
}
|
||||
|
||||
func exampleSignature(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
|
||||
sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sig
|
||||
}
|
||||
|
||||
func invalidNodeFromGolden(t *testing.T) datamodel.Node {
|
||||
t.Helper()
|
||||
|
||||
invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
envelNode := exampleGoldenNode(t)
|
||||
sigPayloadNode, err := envelNode.LookupByIndex(1)
|
||||
require.NoError(t, err)
|
||||
|
||||
node, err := qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.Bytes(invalidSig))
|
||||
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return node
|
||||
}
|
||||
393
token/internal/envelope/ipld.go
Normal file
393
token/internal/envelope/ipld.go
Normal file
@@ -0,0 +1,393 @@
|
||||
// Package envelope provides functions that convert between wire-format
|
||||
// encoding of a [UCAN] token's [Envelope] and the Go type representing
|
||||
// a verified [TokenPayload].
|
||||
//
|
||||
// Encoding functions in this package require a private key as a
|
||||
// parameter so the VarsigHeader can be set and so that a
|
||||
// cryptographic signature can be generated.
|
||||
//
|
||||
// Decoding functions in this package likewise perform the signature
|
||||
// verification using a public key extracted from the TokenPayload as
|
||||
// described by requirement two below.
|
||||
//
|
||||
// Types that wish to be marshaled and unmarshaled from the using
|
||||
// is package have two requirements.
|
||||
//
|
||||
// 1. The type must implement the Tokener interface.
|
||||
//
|
||||
// 2. The IPLD Representation of the type must include an "iss"
|
||||
// field when the TokenPayload is extracted from the Envelope.
|
||||
// This field must contain the string representation of a
|
||||
// "did:key" so that a public key can be extracted from the
|
||||
//
|
||||
// [Envelope]:https://github.com/ucan-wg/spec#envelope
|
||||
// [TokenPayload]: https://github.com/ucan-wg/spec#envelope
|
||||
// [UCAN]: https://ucan.xyz
|
||||
package envelope
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/varsig"
|
||||
)
|
||||
|
||||
const (
|
||||
VarsigHeaderKey = "h"
|
||||
UCANTagPrefix = "ucan/"
|
||||
)
|
||||
|
||||
// Tokener must be implemented by types that wish to be enclosed in a
|
||||
// UCAN Envelope (presumbably one of the UCAN token types).
|
||||
type Tokener interface {
|
||||
// Prototype provides the schema representation for an IPLD type so
|
||||
// that the incoming datamodel.Kinds can be mapped to the appropriate
|
||||
// schema.Kinds.
|
||||
Prototype() schema.TypedPrototype
|
||||
|
||||
// Tag returns the expected key denoting the name of the IPLD node
|
||||
// that should be processed as the token payload while decoding
|
||||
// incoming bytes.
|
||||
Tag() string
|
||||
}
|
||||
|
||||
// Decode unmarshals the input data using the format specified by the
|
||||
// provided codec.Decoder into a Tokener.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Tokener is invalid.
|
||||
func Decode[T Tokener](b []byte, decFn codec.Decoder) (T, error) {
|
||||
node, err := ipld.Decode(b, decFn)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
return FromIPLD[T](node)
|
||||
}
|
||||
|
||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||
func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, error) {
|
||||
node, err := ipld.DecodeStreaming(r, decFn)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
return FromIPLD[T](node)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals the input data into a Tokener.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Tokener is invalid.
|
||||
func FromDagCbor[T Tokener](b []byte) (T, error) {
|
||||
return Decode[T](b, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||
func FromDagCborReader[T Tokener](r io.Reader) (T, error) {
|
||||
return DecodeReader[T](r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagJson unmarshals the input data into a Tokener.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Tokener is invalid.
|
||||
func FromDagJson[T Tokener](b []byte) (T, error) {
|
||||
return Decode[T](b, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||
func FromDagJsonReader[T Tokener](r io.Reader) (T, error) {
|
||||
return DecodeReader[T](r, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromIPLD unwraps a Tokener from the provided IPLD datamodel.Node.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Tokener is invalid.
|
||||
func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
||||
zero := *new(T)
|
||||
|
||||
info, err := Inspect(node)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
if info.Tag != zero.Tag() {
|
||||
return zero, errors.New("data doesn't match the expected type")
|
||||
}
|
||||
|
||||
// This needs to be done before converting this node to its schema
|
||||
// representation (afterwards, the field might be renamed os it's safer
|
||||
// to use the wire name).
|
||||
issuerNode, err := info.tokenPayloadNode.LookupByString("iss")
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
// Replaces the datamodel.Node in tokenPayloadNode with a
|
||||
// schema.TypedNode so that we can cast it to a *token.Token after
|
||||
// unwrapping it.
|
||||
nb := zero.Prototype().Representation().NewBuilder()
|
||||
|
||||
err = nb.AssignNode(info.tokenPayloadNode)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
tokenPayloadNode := nb.Build()
|
||||
|
||||
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
|
||||
if tokenPayload == nil {
|
||||
return zero, errors.New("failed to Unwrap the TokenPayload")
|
||||
}
|
||||
|
||||
tkn, ok := tokenPayload.(T)
|
||||
if !ok {
|
||||
return zero, 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
|
||||
// matches the VarsigHeader and then verify the SigPayload.
|
||||
issuer, err := issuerNode.AsString()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
issuerDID, err := did.Parse(issuer)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
issuerPubKey, err := issuerDID.PubKey()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
if string(info.VarsigHeader) != string(issuerVarsigHeader) {
|
||||
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
||||
}
|
||||
|
||||
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
ok, err = issuerPubKey.Verify(data, info.Signature)
|
||||
if err != nil || !ok {
|
||||
return zero, errors.New("failed to verify the token's signature")
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// Encode marshals a Tokener to the format specified by the provided
|
||||
// codec.Encoder.
|
||||
func Encode(privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) ([]byte, error) {
|
||||
node, err := ToIPLD(privKey, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ipld.Encode(node, encFn)
|
||||
}
|
||||
|
||||
// EncodeWriter is the same as Encode but outputs to an io.Writer instead
|
||||
// of encoding into a []byte.
|
||||
func EncodeWriter(w io.Writer, privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) error {
|
||||
node, err := ToIPLD(privKey, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipld.EncodeStreaming(w, node, encFn)
|
||||
}
|
||||
|
||||
// ToDagCbor marshals the Tokener to the DAG-CBOR format.
|
||||
func ToDagCbor(privKey crypto.PrivKey, token Tokener) ([]byte, error) {
|
||||
return Encode(privKey, token, dagcbor.Encode)
|
||||
}
|
||||
|
||||
// ToDagCborWriter is the same as ToDagCbor but outputs to an io.Writer
|
||||
// instead of encoding into a []byte.
|
||||
func ToDagCborWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error {
|
||||
return EncodeWriter(w, privKey, token, dagcbor.Encode)
|
||||
}
|
||||
|
||||
// ToDagJson marshals the Tokener to the DAG-JSON format.
|
||||
func ToDagJson(privKey crypto.PrivKey, token Tokener) ([]byte, error) {
|
||||
return Encode(privKey, token, dagjson.Encode)
|
||||
}
|
||||
|
||||
// ToDagJsonWriter is the same as ToDagJson but outputs to an io.Writer
|
||||
// instead of encoding into a []byte.
|
||||
func ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error {
|
||||
return EncodeWriter(w, privKey, token, dagjson.Encode)
|
||||
}
|
||||
|
||||
// ToIPLD wraps the Tokener in an IPLD datamodel.Node.
|
||||
func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
|
||||
tokenPayloadNode := bindnode.Wrap(token, token.Prototype().Type()).Representation()
|
||||
|
||||
varsigHeader, err := varsig.Encode(privKey.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, VarsigHeaderKey, qp.Bytes(varsigHeader))
|
||||
qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode))
|
||||
})
|
||||
|
||||
data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := privKey.Sign(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.Bytes(signature))
|
||||
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
||||
})
|
||||
}
|
||||
|
||||
// FindTag inspects the given token IPLD representation and extract the token tag.
|
||||
func FindTag(node datamodel.Node) (string, error) {
|
||||
sigPayloadNode, err := node.LookupByIndex(1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if sigPayloadNode.Kind() != datamodel.Kind_Map {
|
||||
return "", fmt.Errorf("unexpected type instead of map")
|
||||
}
|
||||
|
||||
it := sigPayloadNode.MapIterator()
|
||||
i := 0
|
||||
|
||||
for !it.Done() {
|
||||
if i >= 2 {
|
||||
return "", fmt.Errorf("expected two and only two fields in SigPayload")
|
||||
}
|
||||
i++
|
||||
|
||||
k, _, err := it.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, err := k.AsString()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(key, UCANTagPrefix) {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no token tag found")
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Tag string
|
||||
Signature []byte
|
||||
VarsigHeader []byte
|
||||
sigPayloadNode datamodel.Node // private, we don't want to expose that
|
||||
tokenPayloadNode datamodel.Node // private, we don't want to expose that
|
||||
}
|
||||
|
||||
// Inspect inspects the given token IPLD representation and extract some envelope facts.
|
||||
func Inspect(node datamodel.Node) (Info, error) {
|
||||
var res Info
|
||||
|
||||
signatureNode, err := node.LookupByIndex(0)
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
res.Signature, err = signatureNode.AsBytes()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
res.sigPayloadNode, err = node.LookupByIndex(1)
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
if res.sigPayloadNode.Kind() != datamodel.Kind_Map {
|
||||
return Info{}, fmt.Errorf("unexpected type instead of map")
|
||||
}
|
||||
|
||||
it := res.sigPayloadNode.MapIterator()
|
||||
foundVarsigHeader := false
|
||||
foundTokenPayload := false
|
||||
i := 0
|
||||
|
||||
for !it.Done() {
|
||||
if i >= 2 {
|
||||
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload")
|
||||
}
|
||||
i++
|
||||
|
||||
k, v, err := it.Next()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
key, err := k.AsString()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case key == VarsigHeaderKey:
|
||||
foundVarsigHeader = true
|
||||
res.VarsigHeader, err = v.AsBytes()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
case strings.HasPrefix(key, UCANTagPrefix):
|
||||
foundTokenPayload = true
|
||||
res.Tag = key
|
||||
res.tokenPayloadNode = v
|
||||
default:
|
||||
return Info{}, fmt.Errorf("unexpected key type %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
if i != 2 {
|
||||
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
|
||||
}
|
||||
if !foundVarsigHeader {
|
||||
return Info{}, errors.New("failed to find VarsigHeader field")
|
||||
}
|
||||
if !foundTokenPayload {
|
||||
return Info{}, errors.New("failed to find TokenPayload field")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
209
token/internal/envelope/ipld_test.go
Normal file
209
token/internal/envelope/ipld_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package envelope_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("via FromDagCbor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := golden.Get(t, "example.dagcbor")
|
||||
|
||||
tkn, err := envelope.FromDagCbor[*Example](data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
})
|
||||
|
||||
t.Run("via FromDagJson", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := golden.Get(t, "example.dagjson")
|
||||
|
||||
tkn, err := envelope.FromDagJson[*Example](data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("via ToDagCbor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
|
||||
require.NoError(t, err)
|
||||
golden.AssertBytes(t, data, exampleDAGCBORFilename)
|
||||
})
|
||||
|
||||
t.Run("via ToDagJson", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := envelope.ToDagJson(examplePrivKey(t), newExample(t))
|
||||
require.NoError(t, err)
|
||||
golden.Assert(t, string(data), exampleDAGJSONFilename)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataIn := golden.Get(t, exampleDAGCBORFilename)
|
||||
|
||||
tkn, err := envelope.FromDagCbor[*Example](dataIn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
|
||||
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))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
|
||||
w := &bytes.Buffer{}
|
||||
require.NoError(t, envelope.ToDagCborWriter(w, examplePrivKey(t), newExample(t)))
|
||||
assert.Equal(t, data, w.Bytes())
|
||||
})
|
||||
|
||||
t.Run("via FromDagJson/ToDagJson", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataIn := golden.Get(t, exampleDAGJSONFilename)
|
||||
|
||||
tkn, err := envelope.FromDagJson[*Example](dataIn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
|
||||
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, err := envelope.FromDagJsonReader[*Example](bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
node := invalidNodeFromGolden(t)
|
||||
tkn, err := envelope.FromIPLD[*Example](node)
|
||||
assert.Nil(t, tkn)
|
||||
require.EqualError(t, err, "failed to verify the token's signature")
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msg := []byte("this is a test")
|
||||
|
||||
hash1 := sha256.Sum256(msg)
|
||||
|
||||
hasher := sha256.New()
|
||||
|
||||
for _, b := range msg {
|
||||
hasher.Write([]byte{b})
|
||||
}
|
||||
|
||||
hash2 := hasher.Sum(nil)
|
||||
hash3 := hasher.Sum(nil)
|
||||
|
||||
require.Equal(t, hash1[:], hash2)
|
||||
require.Equal(t, hash1[:], hash3)
|
||||
}
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := golden.Get(t, "example.dagcbor")
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
require.NoError(t, err)
|
||||
|
||||
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := envelope.Inspect(node)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expSig, info.Signature)
|
||||
assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
|
||||
assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
|
||||
}
|
||||
|
||||
func FuzzInspect(f *testing.F) {
|
||||
data, err := os.ReadFile("testdata/example.dagcbor")
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(data)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
_, err = envelope.Inspect(node)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzFindTag(f *testing.F) {
|
||||
data, err := os.ReadFile("testdata/example.dagcbor")
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(data)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
_, err = envelope.FindTag(node)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
})
|
||||
}
|
||||
1
token/internal/envelope/testdata/example.dagcbor
vendored
Normal file
1
token/internal/envelope/testdata/example.dagcbor
vendored
Normal file
@@ -0,0 +1 @@
|
||||
‚X@|úŸÀ½â–ðõ+ÁŠ!µ.®ÿhéÍúGO-ü¬”jÉsyÖsY¨quëiþ“ä°¬Íuý#ò¼’ç˜c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld
|
||||
1
token/internal/envelope/testdata/example.dagjson
vendored
Normal file
1
token/internal/envelope/testdata/example.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}]
|
||||
6
token/internal/envelope/testdata/example.ipldsch
vendored
Normal file
6
token/internal/envelope/testdata/example.ipldsch
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
type DID string
|
||||
|
||||
type Example struct {
|
||||
hello String
|
||||
issuer DID (rename "iss")
|
||||
}
|
||||
Reference in New Issue
Block a user