2024-12-05 20:36:58 -05:00
|
|
|
// Package ucan implements User-Controlled Authorization Network tokens by
|
|
|
|
// fission:
|
|
|
|
// https://whitepaper.fission.codes/access-control/ucan/ucan-tokens
|
|
|
|
//
|
|
|
|
// From the paper:
|
|
|
|
// The UCAN format is designed as an authenticated digraph in some larger
|
|
|
|
// authorization space. The other way to view this is as a function from a set
|
|
|
|
// of authorizations (“UCAN proofs“) to a subset output (“UCAN capabilities”).
|
|
|
|
package ucan
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/ed25519"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt"
|
|
|
|
"github.com/ipfs/go-cid"
|
|
|
|
"github.com/libp2p/go-libp2p/core/crypto"
|
|
|
|
mh "github.com/multiformats/go-multihash"
|
2025-01-06 12:06:10 -05:00
|
|
|
"github.com/onsonr/sonr/crypto/keys"
|
2024-12-05 20:36:58 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// ErrInvalidToken indicates an access token is invalid
|
|
|
|
var ErrInvalidToken = errors.New("invalid access token")
|
|
|
|
|
|
|
|
const (
|
|
|
|
// UCANVersion is the current version of the UCAN spec
|
|
|
|
UCANVersion = "0.7.0"
|
|
|
|
// UCANVersionKey is the key used in version headers for the UCAN spec
|
|
|
|
UCANVersionKey = "ucv"
|
|
|
|
// PrfKey denotes "Proofs" in a UCAN. Stored in JWT Claims
|
|
|
|
PrfKey = "prf"
|
|
|
|
// FctKey denotes "Facts" in a UCAN. Stored in JWT Claims
|
|
|
|
FctKey = "fct"
|
|
|
|
// AttKey denotes "Attenuations" in a UCAN. Stored in JWT Claims
|
|
|
|
AttKey = "att"
|
|
|
|
// CapKey indicates a resource Capability. Used in an attenuation
|
|
|
|
CapKey = "cap"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Token is a JSON Web Token (JWT) that contains special keys that make the
|
|
|
|
// token a UCAN
|
|
|
|
type Token struct {
|
|
|
|
// Entire UCAN as a signed JWT string
|
|
|
|
Raw string
|
2024-12-13 15:10:27 -05:00
|
|
|
Issuer keys.DID
|
|
|
|
Audience keys.DID
|
2024-12-05 20:36:58 -05:00
|
|
|
// the "inputs" to this token, a chain UCAN tokens with broader scopes &
|
|
|
|
// deadlines than this token
|
|
|
|
Proofs []Proof `json:"prf,omitempty"`
|
|
|
|
// the "outputs" of this token, an array of heterogenous resources &
|
|
|
|
// capabilities
|
|
|
|
Attenuations Attenuations `json:"att,omitempty"`
|
|
|
|
// Facts are facts, jack.
|
|
|
|
Facts []Fact `json:"fct,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// CID calculates the cid of a UCAN using the default prefix
|
|
|
|
func (t *Token) CID() (cid.Cid, error) {
|
|
|
|
pref := cid.Prefix{
|
|
|
|
Version: 1,
|
|
|
|
Codec: cid.Raw,
|
|
|
|
MhType: mh.SHA2_256,
|
|
|
|
MhLength: -1, // default length
|
|
|
|
}
|
|
|
|
|
|
|
|
return t.PrefixCID(pref)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PrefixCID calculates the CID of a token with a supplied prefix
|
|
|
|
func (t *Token) PrefixCID(pref cid.Prefix) (cid.Cid, error) {
|
|
|
|
return pref.Sum([]byte(t.Raw))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Claims is the claims component of a UCAN token. UCAN claims are expressed
|
|
|
|
// as a standard JWT claims object with additional special fields
|
|
|
|
type Claims struct {
|
|
|
|
*jwt.StandardClaims
|
|
|
|
// the "inputs" to this token, a chain UCAN tokens with broader scopes &
|
|
|
|
// deadlines than this token
|
|
|
|
// Proofs are UCAN chains, leading back to a self-evident origin token
|
|
|
|
Proofs []Proof `json:"prf,omitempty"`
|
|
|
|
// the "outputs" of this token, an array of heterogenous resources &
|
|
|
|
// capabilities
|
|
|
|
Attenuations Attenuations `json:"att,omitempty"`
|
|
|
|
// Facts are facts, jack.
|
|
|
|
Facts []Fact `json:"fct,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fact is self-evident statement
|
|
|
|
type Fact struct {
|
|
|
|
cidString string
|
|
|
|
value map[string]interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// func (fct *Fact) MarshalJSON() (p[])
|
|
|
|
|
|
|
|
// func (fct *Fact) UnmarshalJSON(p []byte) error {
|
|
|
|
// var str string
|
|
|
|
// if json.Unmarshal(p, &str); err == nil {
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// CIDBytesResolver is a small interface for turning a CID into the bytes
|
|
|
|
// they reference. In practice this may be backed by a network connection that
|
|
|
|
// can fetch CIDs, eg: IPFS.
|
|
|
|
type CIDBytesResolver interface {
|
|
|
|
ResolveCIDBytes(ctx context.Context, id cid.Cid) ([]byte, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Source creates tokens, and provides a verification key for all tokens it
|
|
|
|
// creates
|
|
|
|
//
|
|
|
|
// implementations of Source must conform to the assertion test defined in the
|
|
|
|
// spec subpackage
|
|
|
|
type Source interface {
|
|
|
|
NewOriginToken(audienceDID string, att Attenuations, fct []Fact, notBefore, expires time.Time) (*Token, error)
|
|
|
|
NewAttenuatedToken(parent *Token, audienceDID string, att Attenuations, fct []Fact, notBefore, expires time.Time) (*Token, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type pkSource struct {
|
|
|
|
pk crypto.PrivKey
|
|
|
|
issuerDID string
|
|
|
|
signingMethod jwt.SigningMethod
|
|
|
|
|
|
|
|
verifyKey interface{} // one of: *rsa.PublicKey, *edsa.PublicKey
|
|
|
|
signKey interface{} // one of: *rsa.PrivateKey,
|
|
|
|
}
|
|
|
|
|
|
|
|
// assert pkSource implements tokens at compile time
|
|
|
|
var _ Source = (*pkSource)(nil)
|
|
|
|
|
|
|
|
// NewPrivKeySource creates an authentication interface backed by a single
|
|
|
|
// private key. Intended for a node running as remote, or providing a public API
|
|
|
|
func NewPrivKeySource(privKey crypto.PrivKey) (Source, error) {
|
|
|
|
rawPrivBytes, err := privKey.Raw()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("getting private key bytes: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
methodStr = ""
|
|
|
|
keyType = privKey.Type()
|
|
|
|
signKey interface{}
|
|
|
|
verifyKey interface{}
|
|
|
|
)
|
|
|
|
|
|
|
|
switch keyType {
|
|
|
|
case crypto.RSA:
|
|
|
|
methodStr = "RS256"
|
|
|
|
// TODO(b5) - detect if key is encoded as PEM block, here we're assuming it is
|
|
|
|
signKey, err = x509.ParsePKCS1PrivateKey(rawPrivBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
rawPubBytes, err := privKey.GetPublic().Raw()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("getting raw public key bytes: %w", err)
|
|
|
|
}
|
|
|
|
verifyKeyiface, err := x509.ParsePKIXPublicKey(rawPubBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("parsing public key bytes: %w", err)
|
|
|
|
}
|
|
|
|
var ok bool
|
|
|
|
verifyKey, ok = verifyKeyiface.(*rsa.PublicKey)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("public key is not an RSA key. got type: %T", verifyKeyiface)
|
|
|
|
}
|
|
|
|
case crypto.Ed25519:
|
|
|
|
methodStr = "EdDSA"
|
|
|
|
signKey = ed25519.PrivateKey(rawPrivBytes)
|
|
|
|
rawPubBytes, err := privKey.GetPublic().Raw()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("getting raw public key bytes: %w", err)
|
|
|
|
}
|
|
|
|
verifyKey = ed25519.PublicKey(rawPubBytes)
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unsupported key type for token creation: %q", keyType)
|
|
|
|
}
|
|
|
|
|
|
|
|
issuerDID, err := DIDStringFromPublicKey(privKey.GetPublic())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &pkSource{
|
|
|
|
pk: privKey,
|
|
|
|
signingMethod: jwt.GetSigningMethod(methodStr),
|
|
|
|
verifyKey: verifyKey,
|
|
|
|
signKey: signKey,
|
|
|
|
issuerDID: issuerDID,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *pkSource) NewOriginToken(audienceDID string, att Attenuations, fct []Fact, nbf, exp time.Time) (*Token, error) {
|
|
|
|
return a.newToken(audienceDID, nil, att, fct, nbf, exp)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *pkSource) NewAttenuatedToken(parent *Token, audienceDID string, att Attenuations, fct []Fact, nbf, exp time.Time) (*Token, error) {
|
|
|
|
if !parent.Attenuations.Contains(att) {
|
|
|
|
return nil, fmt.Errorf("scope of ucan attenuations must be less than it's parent")
|
|
|
|
}
|
|
|
|
return a.newToken(audienceDID, append(parent.Proofs, Proof(parent.Raw)), att, fct, nbf, exp)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateToken returns a new JWT token
|
|
|
|
func (a *pkSource) newToken(audienceDID string, prf []Proof, att Attenuations, fct []Fact, nbf, exp time.Time) (*Token, error) {
|
|
|
|
// create a signer for rsa 256
|
|
|
|
t := jwt.New(a.signingMethod)
|
|
|
|
|
|
|
|
// if _, err := did.Parse(audienceDID); err != nil {
|
|
|
|
// return nil, fmt.Errorf("invalid audience DID: %w", err)
|
|
|
|
// }
|
|
|
|
|
|
|
|
t.Header[UCANVersionKey] = UCANVersion
|
|
|
|
|
|
|
|
var (
|
|
|
|
nbfUnix int64
|
|
|
|
expUnix int64
|
|
|
|
)
|
|
|
|
|
|
|
|
if !nbf.IsZero() {
|
|
|
|
nbfUnix = nbf.Unix()
|
|
|
|
}
|
|
|
|
if !exp.IsZero() {
|
|
|
|
expUnix = exp.Unix()
|
|
|
|
}
|
|
|
|
|
|
|
|
// set our claims
|
|
|
|
t.Claims = &Claims{
|
|
|
|
StandardClaims: &jwt.StandardClaims{
|
|
|
|
Issuer: a.issuerDID,
|
|
|
|
Audience: audienceDID,
|
|
|
|
NotBefore: nbfUnix,
|
|
|
|
// set the expire time
|
|
|
|
// see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4
|
|
|
|
ExpiresAt: expUnix,
|
|
|
|
},
|
|
|
|
Attenuations: att,
|
|
|
|
Facts: fct,
|
|
|
|
Proofs: prf,
|
|
|
|
}
|
|
|
|
|
|
|
|
raw, err := t.SignedString(a.signKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Token{
|
|
|
|
Raw: raw,
|
|
|
|
Attenuations: att,
|
|
|
|
Facts: fct,
|
|
|
|
Proofs: prf,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DIDPubKeyResolver turns did:key Decentralized IDentifiers into a public key,
|
|
|
|
// possibly using a network request
|
|
|
|
type DIDPubKeyResolver interface {
|
2024-12-13 15:10:27 -05:00
|
|
|
ResolveDIDKey(ctx context.Context, did string) (keys.DID, error)
|
2024-12-05 20:36:58 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// DIDStringFromPublicKey creates a did:key identifier string from a public key
|
|
|
|
func DIDStringFromPublicKey(pub crypto.PubKey) (string, error) {
|
2024-12-13 15:10:27 -05:00
|
|
|
id, err := keys.NewDID(pub)
|
2024-12-05 20:36:58 -05:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return id.String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// StringDIDPubKeyResolver implements the DIDPubKeyResolver interface without
|
|
|
|
// any network backing. Works if the key string given contains the public key
|
|
|
|
// itself
|
|
|
|
type StringDIDPubKeyResolver struct{}
|
|
|
|
|
|
|
|
// ResolveDIDKey extracts a public key from a did:key string
|
2024-12-13 15:10:27 -05:00
|
|
|
func (StringDIDPubKeyResolver) ResolveDIDKey(ctx context.Context, didStr string) (keys.DID, error) {
|
|
|
|
return keys.Parse(didStr)
|
2024-12-05 20:36:58 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// TokenParser parses a raw string into a Token
|
|
|
|
type TokenParser struct {
|
|
|
|
ap AttenuationConstructorFunc
|
|
|
|
cidr CIDBytesResolver
|
|
|
|
didr DIDPubKeyResolver
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTokenParser constructs a token parser
|
|
|
|
func NewTokenParser(ap AttenuationConstructorFunc, didr DIDPubKeyResolver, cidr CIDBytesResolver) *TokenParser {
|
|
|
|
return &TokenParser{
|
|
|
|
ap: ap,
|
|
|
|
cidr: cidr,
|
|
|
|
didr: didr,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseAndVerify will parse, validate and return a token
|
|
|
|
func (p *TokenParser) ParseAndVerify(ctx context.Context, raw string) (*Token, error) {
|
|
|
|
return p.parseAndVerify(ctx, raw, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *TokenParser) parseAndVerify(ctx context.Context, raw string, child *Token) (*Token, error) {
|
|
|
|
tok, err := jwt.Parse(raw, p.matchVerifyKeyFunc(ctx))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("parsing UCAN: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
mc, ok := tok.Claims.(jwt.MapClaims)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("parser fail")
|
|
|
|
}
|
|
|
|
|
2024-12-13 15:10:27 -05:00
|
|
|
var iss keys.DID
|
2024-12-05 20:36:58 -05:00
|
|
|
// TODO(b5): we're double parsing here b/c the jwt lib we're using doesn't expose
|
|
|
|
// an API (that I know of) for storing parsed issuer / audience
|
|
|
|
if issStr, ok := mc["iss"].(string); ok {
|
2024-12-13 15:10:27 -05:00
|
|
|
iss, err = keys.Parse(issStr)
|
2024-12-05 20:36:58 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf(`"iss" key is not in claims`)
|
|
|
|
}
|
|
|
|
|
2024-12-13 15:10:27 -05:00
|
|
|
var aud keys.DID
|
2024-12-05 20:36:58 -05:00
|
|
|
// TODO(b5): we're double parsing here b/c the jwt lib we're using doesn't expose
|
|
|
|
// an API (that I know of) for storing parsed issuer / audience
|
|
|
|
if audStr, ok := mc["aud"].(string); ok {
|
2024-12-13 15:10:27 -05:00
|
|
|
aud, err = keys.Parse(audStr)
|
2024-12-05 20:36:58 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf(`"aud" key is not in claims`)
|
|
|
|
}
|
|
|
|
|
|
|
|
var att Attenuations
|
|
|
|
if acci, ok := mc[AttKey].([]interface{}); ok {
|
|
|
|
for i, a := range acci {
|
|
|
|
if mapv, ok := a.(map[string]interface{}); ok {
|
|
|
|
a, err := p.ap(mapv)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
att = append(att, a)
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf(`"att[%d]" is not an object`, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf(`"att" key is not an array`)
|
|
|
|
}
|
|
|
|
|
|
|
|
var prf []Proof
|
|
|
|
if prfi, ok := mc[PrfKey].([]interface{}); ok {
|
|
|
|
for i, a := range prfi {
|
|
|
|
if pStr, ok := a.(string); ok {
|
|
|
|
prf = append(prf, Proof(pStr))
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf(`"prf[%d]" is not a string`, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if mc[PrfKey] != nil {
|
|
|
|
return nil, fmt.Errorf(`"prf" key is not an array`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Token{
|
|
|
|
Raw: raw,
|
|
|
|
Issuer: iss,
|
|
|
|
Audience: aud,
|
|
|
|
Attenuations: att,
|
|
|
|
Proofs: prf,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *TokenParser) matchVerifyKeyFunc(ctx context.Context) func(tok *jwt.Token) (interface{}, error) {
|
|
|
|
return func(tok *jwt.Token) (interface{}, error) {
|
|
|
|
mc, ok := tok.Claims.(jwt.MapClaims)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("parser fail")
|
|
|
|
}
|
|
|
|
|
|
|
|
iss, ok := mc["iss"].(string)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf(`"iss" claims key is required`)
|
|
|
|
}
|
|
|
|
|
|
|
|
id, err := p.didr.ResolveDIDKey(ctx, iss)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return id.VerifyKey()
|
|
|
|
}
|
|
|
|
}
|