tokens: read arbitrary token

This commit is contained in:
Michael Muré
2024-10-01 13:23:37 +02:00
parent a2822f02c7
commit f3a5209cec
6 changed files with 265 additions and 185 deletions

View File

@@ -82,7 +82,7 @@ func FromSealedReader(r io.Reader) (*Token, error) {
return tkn, nil return tkn, nil
} }
// Encode marshals a View to the format specified by the provided // Encode marshals a Token to the format specified by the provided
// codec.Encoder. // codec.Encoder.
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) { func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
node, err := t.toIPLD(privKey) node, err := t.toIPLD(privKey)
@@ -103,7 +103,7 @@ func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.En
return ipld.EncodeStreaming(w, node, encFn) return ipld.EncodeStreaming(w, node, encFn)
} }
// ToDagCbor marshals the View to the DAG-CBOR format. // ToDagCbor marshals the Token to the DAG-CBOR format.
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) { func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
return t.Encode(privKey, dagcbor.Encode) return t.Encode(privKey, dagcbor.Encode)
} }
@@ -113,7 +113,7 @@ func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
return t.EncodeWriter(w, privKey, dagcbor.Encode) return t.EncodeWriter(w, privKey, dagcbor.Encode)
} }
// ToDagJson marshals the View to the DAG-JSON format. // ToDagJson marshals the Token to the DAG-JSON format.
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) { func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
return t.Encode(privKey, dagjson.Encode) return t.Encode(privKey, dagjson.Encode)
} }
@@ -124,16 +124,16 @@ func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
} }
// Decode unmarshals the input data using the format specified by the // Decode unmarshals the input data using the format specified by the
// provided codec.Decoder into a View. // provided codec.Decoder into a Token.
// //
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// View is invalid. // Token is invalid.
func Decode(b []byte, decFn codec.Decoder) (*Token, error) { func Decode(b []byte, decFn codec.Decoder) (*Token, error) {
node, err := ipld.Decode(b, decFn) node, err := ipld.Decode(b, decFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return fromIPLD(node) return FromIPLD(node)
} }
// DecodeReader is the same as Decode, but accept an io.Reader. // DecodeReader is the same as Decode, but accept an io.Reader.
@@ -142,13 +142,13 @@ func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return fromIPLD(node) return FromIPLD(node)
} }
// FromDagCbor unmarshals the input data into a View. // FromDagCbor unmarshals the input data into a Token.
// //
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// View is invalid. // Token is invalid.
func FromDagCbor(data []byte) (*Token, error) { func FromDagCbor(data []byte) (*Token, error) {
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data) pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
if err != nil { if err != nil {
@@ -168,10 +168,10 @@ func FromDagCborReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagcbor.Decode) return DecodeReader(r, dagcbor.Decode)
} }
// FromDagJson unmarshals the input data into a View. // FromDagJson unmarshals the input data into a Token.
// //
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// View is invalid. // Token is invalid.
func FromDagJson(data []byte) (*Token, error) { func FromDagJson(data []byte) (*Token, error) {
return Decode(data, dagjson.Decode) return Decode(data, dagjson.Decode)
} }
@@ -181,7 +181,8 @@ func FromDagJsonReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagjson.Decode) return DecodeReader(r, dagjson.Decode)
} }
func fromIPLD(node datamodel.Node) (*Token, error) { // FromIPLD decode the given IPLD representation into a Token.
func FromIPLD(node datamodel.Node) (*Token, error) {
pay, err := envelope.FromIPLD[*tokenPayloadModel](node) pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,105 +1,90 @@
package tokens package tokens
import ( //
"errors" // // EnvelopeInfo describes the fields of an Envelope enclosing a UCAN token.
"fmt" // type EnvelopeInfo struct {
"strings" // Signature []byte
// Tag string
"github.com/ipld/go-ipld-prime" // VarsigHeader []byte
"github.com/ipld/go-ipld-prime/codec" // }
"github.com/ipld/go-ipld-prime/codec/cbor" //
"github.com/ipld/go-ipld-prime/codec/dagcbor" // // InspectEnvelope accepts arbitrary data and attempts to decode it as a
"github.com/ipld/go-ipld-prime/codec/dagjson" // // UCAN token's Envelope.
"github.com/ipld/go-ipld-prime/codec/json" // func Inspect(data []byte) (EnvelopeInfo, error) {
"github.com/ipld/go-ipld-prime/codec/raw" // undef := EnvelopeInfo{}
"github.com/ipld/go-ipld-prime/datamodel" //
"github.com/ucan-wg/go-ucan/tokens/internal/envelope" // node, err := decodeAny(data)
) // if err != nil {
// return undef, err
// EnvelopeInfo describes the fields of an Envelope enclosing a UCAN token. // }
type EnvelopeInfo struct { //
Signature []byte // info, err := envelope.inspect(node)
Tag string // if err != nil {
VarsigHeader []byte // return undef, err
} // }
//
// InspectEnvelope accepts arbitrary data and attempts to decode it as a // iterator := info.SigPayloadNode.MapIterator()
// UCAN token's Envelope. // foundVarsigHeader := false
func Inspect(data []byte) (EnvelopeInfo, error) { // foundTokenPayload := false
undef := EnvelopeInfo{} // tag := ""
// i := 0
node, err := decodeAny(data) //
if err != nil { // for !iterator.Done() {
return undef, err // k, _, err := iterator.Next()
} // if err != nil {
// return undef, err
info, err := envelope.Inspect(node) // }
if err != nil { //
return undef, err // key, err := k.AsString()
} // if err != nil {
// return undef, err
iterator := info.SigPayloadNode.MapIterator() // }
foundVarsigHeader := false //
foundTokenPayload := false // if key == envelope.VarsigHeaderKey {
tag := "" // foundVarsigHeader = true
i := 0 // i++
//
for !iterator.Done() { // continue
k, _, err := iterator.Next() // }
if err != nil { //
return undef, err // if strings.HasPrefix(key, envelope.UCANTagPrefix) {
} // tag = key
// foundTokenPayload = true
key, err := k.AsString() // i++
if err != nil { // }
return undef, err // }
} //
// if i != 2 {
if key == envelope.VarsigHeaderKey { // return undef, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
foundVarsigHeader = true // }
i++ //
// if !foundVarsigHeader {
continue // return undef, errors.New("failed to find VarsigHeader field")
} // }
//
if strings.HasPrefix(key, envelope.UCANTagPrefix) { // if !foundTokenPayload {
tag = key // return undef, errors.New("failed to find TokenPayload field")
foundTokenPayload = true // }
i++ //
} // return EnvelopeInfo{
} // Signature: info.Signature,
// Tag: tag,
if i != 2 { // VarsigHeader: info.VarsigHeader,
return undef, fmt.Errorf("expected two and only two fields in SigPayload: %d", i) // }, nil
} // }
//
if !foundVarsigHeader { // func decodeAny(data []byte) (datamodel.Node, error) {
return undef, errors.New("failed to find VarsigHeader field") // for _, decoder := range []codec.Decoder{
} // dagcbor.Decode,
// dagjson.Decode,
if !foundTokenPayload { // cbor.Decode,
return undef, errors.New("failed to find TokenPayload field") // json.Decode,
} // raw.Decode,
// } {
return EnvelopeInfo{ // if node, err := ipld.Decode(data, decoder); err == nil {
Signature: info.Signature, // return node, nil
Tag: tag, // }
VarsigHeader: info.VarsigHeader, // }
}, nil //
} // return nil, errors.New("failed to decode (any) the provided data")
// }
func decodeAny(data []byte) (datamodel.Node, error) {
for _, decoder := range []codec.Decoder{
dagcbor.Decode,
dagjson.Decode,
cbor.Decode,
json.Decode,
raw.Decode,
} {
if node, err := ipld.Decode(data, decoder); err == nil {
return node, nil
}
}
return nil, errors.New("failed to decode (any) the provided data")
}

View File

@@ -1,34 +1,24 @@
package tokens_test package tokens_test
import ( // func TestInspect(t *testing.T) {
"encoding/base64" // t.Parallel()
"testing" //
// for _, filename := range []string{
"github.com/stretchr/testify/assert" // "example.dagcbor",
"github.com/stretchr/testify/require" // "example.dagjson",
"github.com/ucan-wg/go-ucan/tokens" // } {
"gotest.tools/v3/golden" // t.Run(filename, func(t *testing.T) {
) // t.Parallel()
//
func TestInspect(t *testing.T) { // data := golden.Get(t, filename)
t.Parallel() // expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
// require.NoError(t, err)
for _, filename := range []string{ //
"example.dagcbor", // info, err := tokens.Inspect(data)
"example.dagjson", // require.NoError(t, err)
} { // assert.Equal(t, expSig, info.Signature)
t.Run(filename, func(t *testing.T) { // assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
t.Parallel() // assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
// })
data := golden.Get(t, filename) // }
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg") // }
require.NoError(t, err)
info, err := tokens.Inspect(data)
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)
})
}
}

View File

@@ -16,8 +16,9 @@ import (
"github.com/ipld/go-ipld-prime/schema" "github.com/ipld/go-ipld-prime/schema"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
) )
const ( const (

View File

@@ -27,7 +27,9 @@ package envelope
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"strings"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec" "github.com/ipld/go-ipld-prime/codec"
@@ -141,16 +143,20 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
} }
func fromIPLD[T Tokener](node datamodel.Node) (T, error) { func fromIPLD[T Tokener](node datamodel.Node) (T, error) {
undef := *new(T) zero := *new(T)
info, err := Inspect(node) info, err := inspect(node)
if err != nil { if err != nil {
return undef, err return zero, err
} }
tokenPayloadNode, err := info.SigPayloadNode.LookupByString(undef.Tag()) if info.Tag != zero.Tag() {
return zero, errors.New("data doesn't match the expected type")
}
tokenPayloadNode, err := info.SigPayloadNode.LookupByString(info.Tag)
if err != nil { if err != nil {
return undef, err return zero, err
} }
// This needs to be done before converting this node to its schema // This needs to be done before converting this node to its schema
@@ -158,65 +164,65 @@ func fromIPLD[T Tokener](node datamodel.Node) (T, error) {
// to use the wire name). // to use the wire name).
issuerNode, err := tokenPayloadNode.LookupByString("iss") issuerNode, err := tokenPayloadNode.LookupByString("iss")
if err != nil { if err != nil {
return undef, err return zero, err
} }
// Replaces the datamodel.Node in tokenPayloadNode with a // Replaces the datamodel.Node in tokenPayloadNode with a
// schema.TypedNode so that we can cast it to a *token.Token after // schema.TypedNode so that we can cast it to a *token.Token after
// unwrapping it. // unwrapping it.
nb := undef.Prototype().Representation().NewBuilder() nb := zero.Prototype().Representation().NewBuilder()
err = nb.AssignNode(tokenPayloadNode) err = nb.AssignNode(tokenPayloadNode)
if err != nil { if err != nil {
return undef, err return zero, err
} }
tokenPayloadNode = nb.Build() tokenPayloadNode = nb.Build()
tokenPayload := bindnode.Unwrap(tokenPayloadNode) tokenPayload := bindnode.Unwrap(tokenPayloadNode)
if tokenPayload == nil { if tokenPayload == nil {
return undef, errors.New("failed to Unwrap the TokenPayload") return zero, errors.New("failed to Unwrap the TokenPayload")
} }
tkn, ok := tokenPayload.(T) tkn, ok := tokenPayload.(T)
if !ok { if !ok {
return undef, errors.New("failed to assert the TokenPayload type as *token.Token") 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 // Check that the issuer's DID contains a public key with a type that
// matches the VarsigHeader and then verify the SigPayload. // matches the VarsigHeader and then verify the SigPayload.
issuer, err := issuerNode.AsString() issuer, err := issuerNode.AsString()
if err != nil { if err != nil {
return undef, err return zero, err
} }
issuerDID, err := did.Parse(issuer) issuerDID, err := did.Parse(issuer)
if err != nil { if err != nil {
return undef, err return zero, err
} }
issuerPubKey, err := issuerDID.PubKey() issuerPubKey, err := issuerDID.PubKey()
if err != nil { if err != nil {
return undef, err return zero, err
} }
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type()) issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
if err != nil { if err != nil {
return undef, err return zero, err
} }
if string(info.VarsigHeader) != string(issuerVarsigHeader) { if string(info.VarsigHeader) != string(issuerVarsigHeader) {
return undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type") return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
} }
data, err := ipld.Encode(info.SigPayloadNode, dagcbor.Encode) data, err := ipld.Encode(info.SigPayloadNode, dagcbor.Encode)
if err != nil { if err != nil {
return undef, err return zero, err
} }
ok, err = issuerPubKey.Verify(data, info.Signature) ok, err = issuerPubKey.Verify(data, info.Signature)
if err != nil || !ok { if err != nil || !ok {
return undef, errors.New("failed to verify the token's signature") return zero, errors.New("failed to verify the token's signature")
} }
return tkn, nil return tkn, nil
@@ -296,42 +302,109 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
}) })
} }
type Info struct { // FindTag inspect 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
}
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 Signature []byte
SigPayloadNode datamodel.Node SigPayloadNode datamodel.Node
VarsigHeader []byte VarsigHeader []byte
} }
func Inspect(node datamodel.Node) (Info, error) { func inspect(node datamodel.Node) (info, error) {
var undef Info var res info
signatureNode, err := node.LookupByIndex(0) signatureNode, err := node.LookupByIndex(0)
if err != nil { if err != nil {
return undef, err return info{}, err
} }
signature, err := signatureNode.AsBytes() res.Signature, err = signatureNode.AsBytes()
if err != nil { if err != nil {
return undef, err return info{}, err
} }
sigPayloadNode, err := node.LookupByIndex(1) res.SigPayloadNode, err = node.LookupByIndex(1)
if err != nil { if err != nil {
return undef, err return info{}, err
} }
varsigHeaderNode, err := sigPayloadNode.LookupByString(VarsigHeaderKey) it := res.SigPayloadNode.MapIterator()
if err != nil { foundVarsigHeader := false
return undef, err foundTokenPayload := false
} i := 0
varsigHeader, err := varsigHeaderNode.AsBytes()
if err != nil { for !it.Done() {
return undef, err 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
default:
return info{}, fmt.Errorf("unexpected key type %q", key)
}
} }
return Info{ if i != 2 {
Signature: signature, return info{}, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
SigPayloadNode: sigPayloadNode, }
VarsigHeader: varsigHeader, if !foundVarsigHeader {
}, nil return info{}, errors.New("failed to find VarsigHeader field")
}
if !foundTokenPayload {
return info{}, errors.New("failed to find TokenPayload field")
}
return res, nil
} }

30
tokens/read.go Normal file
View File

@@ -0,0 +1,30 @@
package tokens
import (
"fmt"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ucan-wg/go-ucan/tokens/delegation"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
)
func FromDagCbor(b []byte) (any, error) {
node, err := ipld.Decode(b, dagcbor.Decode)
if err != nil {
return nil, err
}
tag, err := envelope.FindTag(node)
if err != nil {
return nil, err
}
switch tag {
case delegation.Tag:
return delegation.FromIPLD(node)
default:
return nil, fmt.Errorf(`unknown tag "%s"`, tag)
}
}