sonr/crypto/ucan/token.go
Prad Nukala ac8c153b28
feature/1120 leverage service authorization (#1188)
* refactor: remove redundant branch trigger for scheduled releases

* refactor: simplify process-compose commands and improve logging

* refactor: remove redundant  command

* refactor: remove unused error variables and simplify database configuration

* feat: introduce task runner for project automation

* refactor: Remove hardcoded action and method from form components

* refactor: move server setup to main.go and add prometheus metrics

* refactor: move index handlers to render handlers

* refactor: improve user identification logic in gateway and vault handlers

* refactor: rename TitleDescription to TitleDesc for consistency

* feat: integrate go-useragent library for enhanced user agent parsing

* feat: enhance initial view rendering based on device type

* feat: Add support for PostgreSQL database

* fix: Use formatPsqlDSN() to properly set PostgreSQL DSN from command flags

* feat: Add PostgreSQL support with fallback to SQLite in NewGormDB

* feat: Add PostgreSQL connection validation with SQLite fallback

* chore: update golang.org/x/crypto dependency to v0.31.0

* feat: add PKL-based configuration initialization

* refactor: improve file naming consistency in cmd/sonrd

* refactor: Improve init-pkl command with safer config file generation and error handling

* fix: add logging for pkl evaluation results

* refactor: Move credential handling to gateway context

* refactor: Migrate session models to gateway package

* refactor: rename models and update User model

* chore: initial commit for address and pubkey functionality

* refactor: move pubkey package to keys package

* refactor: Rename models and add resolver service

* feat: add gRPC clients for bank, DID, DWN, and SVC modules

* refactor: Migrate title and description components from text package to hero package

* refactor: improve file naming conventions

* feat: add user credential validation

* refactor: rename registration handlers and routes for clarity

* <no value>

* refactor: Decouple database and IPFS interactions from server setup

* refactor: Migrate configuration from class-based to TOML-based structure

* refactor: move network configuration files to sonr.net module

* feature/1120-leverage-service-authorization

* fix: correct DID identifier creation function name

* feat: add compressed and uncompressed public keys to keyset

* refactor: move address packages to crypto/address

* feat: implement pubkey verification

* refactor: remove ECDSA-related functions from keyshare and protocol modules

* feat: Implement ECDSA signature serialization

* <no value>

* feat: add vault service for IPFS token storage

* refactor: update ucan codec to use new DID generation method

* refactor: refactor key management and move address parsers to keys package

* refactor: rename key parsers and move to parsers package

* fix: resolved import issues with the new spec

* feat: improve user onboarding experience by updating button text and functionality

* refactor: update point marshaling and unmarshaling methods to use JSON

* refactor: remove unnecessary DID method from PubKey

* refactor: Rename and refactor MPC key generation functions

* test: Add comprehensive test suite for keyshare generation and validation

* test: Fix keyshare role validation and encoding tests

* feat: Update key share role tests with enclave initialization validation

* test(mpc): refactor tests to focus on public API and remove internal role checks

* refactor: Remove unnecessary role check in initKeyEnclave function

* fix: Enforce strict order for validator and user keyshares in enclave initialization

* fix: Update codec_test to match latest codec implementation

* refactor: Update KeyEnclave to use string-based key shares and improve error handling

* fix: Refactor MPC enclave to use string-based encoding and simplify key management

* refactor: Remove redundant keyshare decoding tests in codec_test.go

* fix: Resolve type conversion issues in MPC crypto enclave initialization

* fix: Convert CID to byte slice in addEnclaveIPFS function

* fix: Resolve type conversion and constant definition errors in MPC crypto utils

* refactor: Simplify KeyShare encoding and role handling in MPC codec

* fix: Resolve JSON unmarshaling type mismatch in KeyShare.Message()

* fix: Refactor KeyEnclave to use struct and Enclave interface

* fix: Resolve type and naming conflicts in MPC crypto package

* refactor: Update codec_test.go to use new KeyEnclave struct fields

* refactor: remove keyshare encoding and decoding logic

* refactor: Remove unused JSON marshaling functions for curve points

* fix: Improve signature serialization and deserialization in MPC crypto

This commit addresses several issues with signature handling:
- Fixed signature length to 65 bytes
- Added proper padding for R and S values
- Added nil and zero value checks
- Improved error messages for signature parsing

The changes ensure more robust signature encoding and decoding, preventing potential nil pointer and invalid signature issues.

* fix: Update signature serialization to match protocol test approach

* refactor: Simplify KeyEnclave struct and improve message handling

* fix: Improve signature serialization and verification in MPC crypto module

* refactor: Simplify enclave validation using IsValid method in test

* refactor: Add marshaling and comprehensive tests for KeyEnclave

* feat: Add JSON marshaling support for Point in KeyEnclave

* refactor: Rename KeyEnclave to Enclave and update related functions

* refactor: Update PubKey verification to use SHA3-256 hashing

* test: Add comprehensive tests for DID and PubKey implementations

* refactor: simplify DID key retrieval

* test: refactor CI workflow and remove unused DIDAuth middleware

* The changes look good! The updated workflows will now:

1. Run tests on push to master
2. Bump the version if the commit doesn't already start with 'bump:'
3. Trigger a release workflow automatically with the new version tag
4. Create and publish the release

A few things to note:
- Make sure you have the `peter-evans/repository-dispatch` action installed/available
- The `commitizen-tools/commitizen-action` should output the new tag for this to work
- Ensure your release workflow can handle the repository dispatch event

Would you like me to review or suggest any additional modifications to the workflows?

* ci(github actions): add build stage dependency for tests

* fix(workflow): update workflow to trigger on PR edits

* test: Update unit test dependencies

* ci: Add GoReleaser dry-run check for merge group events

* test: remove unnecessary dependencies between test jobs

* ci: Make race and coverage tests depend on build tests
2024-12-13 20:10:27 +00:00

401 lines
11 KiB
Go

// 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"
"github.com/onsonr/sonr/crypto/keys"
)
// 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
Issuer keys.DID
Audience keys.DID
// 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 {
ResolveDIDKey(ctx context.Context, did string) (keys.DID, error)
}
// DIDStringFromPublicKey creates a did:key identifier string from a public key
func DIDStringFromPublicKey(pub crypto.PubKey) (string, error) {
id, err := keys.NewDID(pub)
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
func (StringDIDPubKeyResolver) ResolveDIDKey(ctx context.Context, didStr string) (keys.DID, error) {
return keys.Parse(didStr)
}
// 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")
}
var iss keys.DID
// 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 {
iss, err = keys.Parse(issStr)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf(`"iss" key is not in claims`)
}
var aud keys.DID
// 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 {
aud, err = keys.Parse(audStr)
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()
}
}