Skip to content

Commit 942f343

Browse files
committed
WIP
1 parent 4b99c9f commit 942f343

File tree

8 files changed

+937
-0
lines changed

8 files changed

+937
-0
lines changed

token/attestation/attestation.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Package attestation implements the UCAN [attestation] specification with
2+
// an immutable Token type as well as methods to convert the Token to and
3+
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
4+
// should most commonly be used for transport and storage.
5+
//
6+
// [envelope]: https://github.com/ucan-wg/spec#envelope
7+
// [attestation]: TBD
8+
package attestation
9+
10+
import (
11+
"encoding/base64"
12+
"errors"
13+
"fmt"
14+
"strings"
15+
"time"
16+
17+
"github.com/MetaMask/go-did-it"
18+
"github.com/ipfs/go-cid"
19+
20+
"github.com/ucan-wg/go-ucan/pkg/args"
21+
"github.com/ucan-wg/go-ucan/pkg/command"
22+
"github.com/ucan-wg/go-ucan/pkg/meta"
23+
"github.com/ucan-wg/go-ucan/token/delegation"
24+
"github.com/ucan-wg/go-ucan/token/internal/nonce"
25+
"github.com/ucan-wg/go-ucan/token/internal/parse"
26+
)
27+
28+
// Token is an immutable type that holds the fields of a UCAN attestation.
29+
type Token struct {
30+
// The DID of the Invoker
31+
issuer did.DID
32+
// TODO: should this exist?
33+
// audience did.DID
34+
35+
// TODO: make specific type
36+
claims map[string]interface{}
37+
38+
// Arbitrary Metadata
39+
meta *meta.Meta
40+
41+
// A unique, random nonce
42+
nonce []byte
43+
// The timestamp at which the Invocation becomes invalid
44+
expiration *time.Time
45+
// The timestamp at which the Invocation was created
46+
issuedAt *time.Time
47+
}
48+
49+
// New creates an attestation Token with the provided options.
50+
//
51+
// If no nonce is provided, a random 12-byte nonce is generated. Use the
52+
// WithNonce or WithEmptyNonce options to specify provide your own nonce
53+
// or to leave the nonce empty respectively.
54+
//
55+
// If no IssuedAt is provided, the current time is used. Use the
56+
// IssuedAt or WithIssuedAtIn Options to specify a different time
57+
// or the WithoutIssuedAt Option to clear the Token's IssuedAt field.
58+
//
59+
// With the exception of the WithMeta option, all others will overwrite
60+
// the previous contents of their target field.
61+
//
62+
// You can read it as "(Issuer - I) attest (arbitrary claim)".
63+
func New(iss did.DID, opts ...Option) (*Token, error) {
64+
iat := time.Now()
65+
66+
tkn := Token{
67+
issuer: iss,
68+
subject: sub,
69+
command: cmd,
70+
arguments: args.New(),
71+
proof: prf,
72+
meta: meta.NewMeta(),
73+
nonce: nil,
74+
issuedAt: &iat,
75+
}
76+
77+
for _, opt := range opts {
78+
if err := opt(&tkn); err != nil {
79+
return nil, err
80+
}
81+
}
82+
83+
var err error
84+
if len(tkn.nonce) == 0 {
85+
tkn.nonce, err = nonce.Generate()
86+
if err != nil {
87+
return nil, err
88+
}
89+
}
90+
91+
if err := tkn.validate(); err != nil {
92+
return nil, err
93+
}
94+
95+
return &tkn, nil
96+
}
97+
98+
// Issuer returns the did.DID representing the Token's issuer.
99+
func (t *Token) Issuer() did.DID {
100+
return t.issuer
101+
}
102+
103+
// Meta returns the Token's metadata.
104+
func (t *Token) Meta() meta.ReadOnly {
105+
return t.meta.ReadOnly()
106+
}
107+
108+
// Nonce returns the random Nonce encapsulated in this Token.
109+
func (t *Token) Nonce() []byte {
110+
return t.nonce
111+
}
112+
113+
// Expiration returns the time at which the Token expires.
114+
func (t *Token) Expiration() *time.Time {
115+
return t.expiration
116+
}
117+
118+
// IssuedAt returns the time.Time at which the invocation token was
119+
// created.
120+
func (t *Token) IssuedAt() *time.Time {
121+
return t.issuedAt
122+
}
123+
124+
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
125+
// This does NOT do any other kind of verifications.
126+
func (t *Token) IsValidNow() bool {
127+
return t.IsValidAt(time.Now())
128+
}
129+
130+
// IsValidAt verifies that the token can be used at the given time, based on expiration or "not before" fields.
131+
// This does NOT do any other kind of verifications.
132+
func (t *Token) IsValidAt(ti time.Time) bool {
133+
if t.expiration != nil && ti.After(*t.expiration) {
134+
return false
135+
}
136+
return true
137+
}
138+
139+
func (t *Token) String() string {
140+
var res strings.Builder
141+
142+
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
143+
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
144+
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
145+
res.WriteString(fmt.Sprintf("Expiration: %v\n", t.Expiration()))
146+
res.WriteString(fmt.Sprintf("Issued At: %v\n", t.IssuedAt()))
147+
148+
return res.String()
149+
}
150+
151+
func (t *Token) validate() error {
152+
var errs error
153+
154+
requiredDID := func(id did.DID, fieldname string) {
155+
if id == nil {
156+
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
157+
}
158+
}
159+
160+
requiredDID(t.issuer, "Issuer")
161+
162+
if len(t.nonce) < 12 {
163+
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
164+
}
165+
166+
return errs
167+
}
168+
169+
// tokenFromModel build a decoded view of the raw IPLD data.
170+
// This function also serves as validation.
171+
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
172+
var (
173+
tkn Token
174+
err error
175+
)
176+
177+
if tkn.issuer, err = did.Parse(m.Iss); err != nil {
178+
return nil, fmt.Errorf("parse iss: %w", err)
179+
}
180+
181+
if tkn.subject, err = did.Parse(m.Sub); err != nil {
182+
return nil, fmt.Errorf("parse subject: %w", err)
183+
}
184+
185+
if tkn.audience, err = parse.OptionalDID(m.Aud); err != nil {
186+
return nil, fmt.Errorf("parse audience: %w", err)
187+
}
188+
189+
if tkn.command, err = command.Parse(m.Cmd); err != nil {
190+
return nil, fmt.Errorf("parse command: %w", err)
191+
}
192+
193+
if len(m.Nonce) == 0 {
194+
return nil, fmt.Errorf("nonce is required")
195+
}
196+
tkn.nonce = m.Nonce
197+
198+
tkn.arguments = m.Args
199+
if err := tkn.arguments.Validate(); err != nil {
200+
return nil, fmt.Errorf("invalid arguments: %w", err)
201+
}
202+
203+
tkn.proof = m.Prf
204+
205+
tkn.meta = m.Meta
206+
if tkn.meta == nil {
207+
tkn.meta = meta.NewMeta()
208+
}
209+
210+
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
211+
if err != nil {
212+
return nil, fmt.Errorf("parse expiration: %w", err)
213+
}
214+
215+
tkn.issuedAt, err = parse.OptionalTimestamp(m.Iat)
216+
if err != nil {
217+
return nil, fmt.Errorf("parse IssuedAt: %w", err)
218+
}
219+
220+
tkn.cause = m.Cause
221+
222+
if err := tkn.validate(); err != nil {
223+
return nil, err
224+
}
225+
226+
return &tkn, nil
227+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
type DID string
2+
3+
# The Attestation payload
4+
type Payload struct {
5+
# Issuer DID (sender)
6+
iss DID
7+
# Audience DID (receiver) TODO: should that exist?
8+
# aud DID
9+
10+
# Arbitrary claims
11+
attest {String: Any}
12+
13+
# A unique, random nonce
14+
nonce Bytes
15+
16+
# Arbitrary Metadata
17+
meta optional {String : Any}
18+
19+
# A unique, random nonce
20+
nonce optional Bytes
21+
# The timestamp at which the Invocation becomes invalid
22+
exp nullable Int
23+
# The Timestamp at which the Invocation was created
24+
iat optional Int
25+
}

0 commit comments

Comments
 (0)