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 }