github.com/storacha/go-ucanto@v0.7.2/ucan/lib.go (about)

     1  package ucan
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/ipld/go-ipld-prime/datamodel"
     8  	"github.com/storacha/go-ucanto/ucan/crypto/signature"
     9  	pdm "github.com/storacha/go-ucanto/ucan/datamodel/payload"
    10  	udm "github.com/storacha/go-ucanto/ucan/datamodel/ucan"
    11  	"github.com/storacha/go-ucanto/ucan/formatter"
    12  )
    13  
    14  const version = "0.9.1"
    15  
    16  // Option is an option configuring a UCAN.
    17  type Option func(cfg *ucanConfig) error
    18  
    19  type ucanConfig struct {
    20  	exp   *UTCUnixTimestamp
    21  	noexp bool
    22  	nbf   UTCUnixTimestamp
    23  	nnc   string
    24  	fct   []FactBuilder
    25  	prf   []Link
    26  }
    27  
    28  // WithExpiration configures the expiration time in UTC seconds since Unix
    29  // epoch.
    30  func WithExpiration(exp UTCUnixTimestamp) Option {
    31  	return func(cfg *ucanConfig) error {
    32  		cfg.exp = &exp
    33  		cfg.noexp = false
    34  		return nil
    35  	}
    36  }
    37  
    38  // WithNoExpiration configures the UCAN to never expire.
    39  //
    40  // WARNING: this will cause the delegation to be valid FOREVER, unless revoked.
    41  func WithNoExpiration() Option {
    42  	return func(cfg *ucanConfig) error {
    43  		cfg.exp = nil
    44  		cfg.noexp = true
    45  		return nil
    46  	}
    47  }
    48  
    49  // WithNotBefore configures the time in UTC seconds since Unix epoch when the
    50  // UCAN will become valid.
    51  func WithNotBefore(nbf int) Option {
    52  	return func(cfg *ucanConfig) error {
    53  		cfg.nbf = nbf
    54  		return nil
    55  	}
    56  }
    57  
    58  // WithNonce configures the nonce value for the UCAN.
    59  func WithNonce(nnc string) Option {
    60  	return func(cfg *ucanConfig) error {
    61  		cfg.nnc = nnc
    62  		return nil
    63  	}
    64  }
    65  
    66  // WithFacts configures the facts for the UCAN.
    67  func WithFacts(fct []FactBuilder) Option {
    68  	return func(cfg *ucanConfig) error {
    69  		cfg.fct = fct
    70  		return nil
    71  	}
    72  }
    73  
    74  // WithProof configures the proofs for the UCAN.
    75  func WithProof(prf ...Link) Option {
    76  	return func(cfg *ucanConfig) error {
    77  		cfg.prf = prf
    78  		return nil
    79  	}
    80  }
    81  
    82  // MapBuilder builds a map of string => datamodel.Node from the underlying data.
    83  type MapBuilder interface {
    84  	ToIPLD() (map[string]datamodel.Node, error)
    85  }
    86  
    87  type FactBuilder = MapBuilder
    88  
    89  // CaveatBuilder builds a datamodel.Node from the underlying data.
    90  type CaveatBuilder interface {
    91  	ToIPLD() (datamodel.Node, error)
    92  }
    93  
    94  // Issue creates a new signed token with a given issuer. If expiration is
    95  // not set it defaults to 30 seconds from now.
    96  func Issue[C CaveatBuilder](issuer Signer, audience Principal, capabilities []Capability[C], options ...Option) (View, error) {
    97  	cfg := ucanConfig{}
    98  	for _, opt := range options {
    99  		if err := opt(&cfg); err != nil {
   100  			return nil, err
   101  		}
   102  	}
   103  
   104  	var exp *int
   105  	if !cfg.noexp {
   106  		if cfg.exp == nil {
   107  			in30s := int(Now() + 30)
   108  			exp = &in30s
   109  		} else {
   110  			exp = cfg.exp
   111  		}
   112  	}
   113  
   114  	var capsmdl []udm.CapabilityModel
   115  	for _, cap := range capabilities {
   116  		nb, err := cap.Nb().ToIPLD()
   117  		if err != nil {
   118  			return nil, fmt.Errorf("building caveats: %w", err)
   119  		}
   120  		m := udm.CapabilityModel{
   121  			With: cap.With(),
   122  			Can:  cap.Can(),
   123  			Nb:   nb,
   124  		}
   125  		capsmdl = append(capsmdl, m)
   126  	}
   127  
   128  	var prfstrs []string
   129  	for _, link := range cfg.prf {
   130  		prfstrs = append(prfstrs, link.String())
   131  	}
   132  
   133  	var fctsmdl []udm.FactModel
   134  	for _, f := range cfg.fct {
   135  		vals, err := f.ToIPLD()
   136  		if err != nil {
   137  			return nil, fmt.Errorf("building fact: %w", err)
   138  		}
   139  		var keys []string
   140  		for k := range vals {
   141  			keys = append(keys, k)
   142  		}
   143  		fctsmdl = append(fctsmdl, udm.FactModel{
   144  			Keys:   keys,
   145  			Values: vals,
   146  		})
   147  	}
   148  
   149  	payload := pdm.PayloadModel{
   150  		Iss: issuer.DID().String(),
   151  		Aud: audience.DID().String(),
   152  		Att: capsmdl,
   153  		Prf: prfstrs,
   154  		Exp: exp,
   155  		Fct: fctsmdl,
   156  	}
   157  	if cfg.nnc != "" {
   158  		payload.Nnc = &cfg.nnc
   159  	}
   160  	if cfg.nbf != 0 {
   161  		payload.Nbf = &cfg.nbf
   162  	}
   163  	bytes, err := encodeSignaturePayload(payload, version, issuer.SignatureAlgorithm())
   164  	if err != nil {
   165  		return nil, fmt.Errorf("encoding signature payload: %w", err)
   166  	}
   167  
   168  	model := udm.UCANModel{
   169  		V:   version,
   170  		S:   issuer.Sign(bytes).Bytes(),
   171  		Iss: issuer.DID().Bytes(),
   172  		Aud: audience.DID().Bytes(),
   173  		Att: capsmdl,
   174  		Prf: cfg.prf,
   175  		Exp: exp,
   176  		Fct: fctsmdl,
   177  	}
   178  	if cfg.nnc != "" {
   179  		model.Nnc = &cfg.nnc
   180  	}
   181  	if cfg.nbf != 0 {
   182  		model.Nbf = &cfg.nbf
   183  	}
   184  	return NewUCAN(&model)
   185  }
   186  
   187  func encodeSignaturePayload(payload pdm.PayloadModel, version string, algorithm string) ([]byte, error) {
   188  	str, err := formatter.FormatSignPayload(payload, version, algorithm)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	return []byte(str), nil
   193  }
   194  
   195  func VerifySignature(ucan View, verifier Verifier) (bool, error) {
   196  	alg, err := signature.CodeName(ucan.Signature().Code())
   197  	if err != nil {
   198  		return false, err
   199  	}
   200  
   201  	var prfstrs []string
   202  	for _, link := range ucan.Proofs() {
   203  		prfstrs = append(prfstrs, link.String())
   204  	}
   205  
   206  	payload := pdm.PayloadModel{
   207  		Iss: ucan.Issuer().DID().String(),
   208  		Aud: ucan.Audience().DID().String(),
   209  		Att: ucan.Model().Att,
   210  		Prf: prfstrs,
   211  		Exp: ucan.Expiration(),
   212  		Fct: ucan.Model().Fct,
   213  		Nnc: ucan.Model().Nnc,
   214  		Nbf: ucan.Model().Nbf,
   215  	}
   216  
   217  	msg, err := encodeSignaturePayload(payload, ucan.Version(), alg)
   218  	if err != nil {
   219  		return false, err
   220  	}
   221  
   222  	return ucan.Issuer().DID() == verifier.DID() && verifier.Verify(msg, ucan.Signature()), nil
   223  }
   224  
   225  // IsExpired checks if a UCAN is expired.
   226  func IsExpired(ucan UCAN) bool {
   227  	exp := ucan.Expiration()
   228  	if exp == nil {
   229  		return false
   230  	}
   231  	return *exp <= Now()
   232  }
   233  
   234  // IsTooEarly checks if a UCAN is not active yet.
   235  func IsTooEarly(ucan UCAN) bool {
   236  	nbf := ucan.NotBefore()
   237  	return nbf != 0 && Now() <= nbf
   238  }
   239  
   240  // Now returns a UTC Unix timestamp for comparing it against time window of the
   241  // UCAN.
   242  func Now() UTCUnixTimestamp {
   243  	return UTCUnixTimestamp(time.Now().Unix())
   244  }