git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/session/common.go (about) 1 package session 2 3 import ( 4 "bytes" 5 "crypto/ecdsa" 6 "errors" 7 "fmt" 8 9 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" 10 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" 11 frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" 12 frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" 13 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" 14 "github.com/google/uuid" 15 ) 16 17 type commonData struct { 18 idSet bool 19 id uuid.UUID 20 21 issuerSet bool 22 issuer user.ID 23 24 lifetimeSet bool 25 iat, nbf, exp uint64 26 27 authKey []byte 28 29 sigSet bool 30 sig refs.Signature 31 } 32 33 type contextReader func(session.TokenContext, bool) error 34 35 // reads commonData and custom context from the session.Token message. 36 // If checkFieldPresence is set, returns an error on absence of any protocol-required 37 // field. Verifies format of any presented field according to FrostFS API V2 protocol. 38 // Calls contextReader if session context is set. Passes checkFieldPresence into contextReader. 39 func (x *commonData) readFromV2(m session.Token, checkFieldPresence bool, r contextReader) error { 40 var err error 41 42 body := m.GetBody() 43 if checkFieldPresence && body == nil { 44 return errors.New("missing token body") 45 } 46 47 binID := body.GetID() 48 if x.idSet = len(binID) > 0; x.idSet { 49 err = x.id.UnmarshalBinary(binID) 50 if err != nil { 51 return fmt.Errorf("invalid session ID: %w", err) 52 } else if ver := x.id.Version(); ver != 4 { 53 return fmt.Errorf("invalid session UUID version %d", ver) 54 } 55 } else if checkFieldPresence { 56 return errors.New("missing session ID") 57 } 58 59 issuer := body.GetOwnerID() 60 if x.issuerSet = issuer != nil; x.issuerSet { 61 err = x.issuer.ReadFromV2(*issuer) 62 if err != nil { 63 return fmt.Errorf("invalid session issuer: %w", err) 64 } 65 } else if checkFieldPresence { 66 return errors.New("missing session issuer") 67 } 68 69 lifetime := body.GetLifetime() 70 if x.lifetimeSet = lifetime != nil; x.lifetimeSet { 71 x.iat = lifetime.GetIat() 72 x.nbf = lifetime.GetNbf() 73 x.exp = lifetime.GetExp() 74 } else if checkFieldPresence { 75 return errors.New("missing token lifetime") 76 } 77 78 x.authKey = body.GetSessionKey() 79 if checkFieldPresence && len(x.authKey) == 0 { 80 return errors.New("missing session public key") 81 } 82 83 c := body.GetContext() 84 if c != nil { 85 err = r(c, checkFieldPresence) 86 if err != nil { 87 return fmt.Errorf("invalid context: %w", err) 88 } 89 } else if checkFieldPresence { 90 return errors.New("missing session context") 91 } 92 93 sig := m.GetSignature() 94 if x.sigSet = sig != nil; sig != nil { 95 x.sig = *sig 96 } else if checkFieldPresence { 97 return errors.New("missing body signature") 98 } 99 100 return nil 101 } 102 103 type contextWriter func() session.TokenContext 104 105 func (x commonData) fillBody(w contextWriter) *session.TokenBody { 106 var body session.TokenBody 107 108 if x.idSet { 109 binID, err := x.id.MarshalBinary() 110 if err != nil { 111 panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err)) 112 } 113 114 body.SetID(binID) 115 } 116 117 if x.issuerSet { 118 var issuer refs.OwnerID 119 x.issuer.WriteToV2(&issuer) 120 121 body.SetOwnerID(&issuer) 122 } 123 124 if x.lifetimeSet { 125 var lifetime session.TokenLifetime 126 lifetime.SetIat(x.iat) 127 lifetime.SetNbf(x.nbf) 128 lifetime.SetExp(x.exp) 129 130 body.SetLifetime(&lifetime) 131 } 132 133 body.SetSessionKey(x.authKey) 134 135 body.SetContext(w()) 136 137 return &body 138 } 139 140 func (x commonData) writeToV2(m *session.Token, w contextWriter) { 141 body := x.fillBody(w) 142 143 m.SetBody(body) 144 145 var sig *refs.Signature 146 147 if x.sigSet { 148 sig = &x.sig 149 } 150 151 m.SetSignature(sig) 152 } 153 154 func (x commonData) signedData(w contextWriter) []byte { 155 return x.fillBody(w).StableMarshal(nil) 156 } 157 158 func (x *commonData) sign(key ecdsa.PrivateKey, w contextWriter) error { 159 user.IDFromKey(&x.issuer, key.PublicKey) 160 x.issuerSet = true 161 162 var sig frostfscrypto.Signature 163 164 err := sig.Calculate(frostfsecdsa.Signer(key), x.signedData(w)) 165 if err != nil { 166 return err 167 } 168 169 sig.WriteToV2(&x.sig) 170 x.sigSet = true 171 172 return nil 173 } 174 175 func (x commonData) verifySignature(w contextWriter) bool { 176 if !x.sigSet { 177 return false 178 } 179 180 var sig frostfscrypto.Signature 181 182 // TODO: (#233) check owner<->key relation 183 return sig.ReadFromV2(x.sig) == nil && sig.Verify(x.signedData(w)) 184 } 185 186 func (x commonData) marshal(w contextWriter) []byte { 187 var m session.Token 188 x.writeToV2(&m, w) 189 190 return m.StableMarshal(nil) 191 } 192 193 func (x *commonData) unmarshal(data []byte, r contextReader) error { 194 var m session.Token 195 196 err := m.Unmarshal(data) 197 if err != nil { 198 return err 199 } 200 201 return x.readFromV2(m, false, r) 202 } 203 204 func (x commonData) marshalJSON(w contextWriter) ([]byte, error) { 205 var m session.Token 206 x.writeToV2(&m, w) 207 208 return m.MarshalJSON() 209 } 210 211 func (x *commonData) unmarshalJSON(data []byte, r contextReader) error { 212 var m session.Token 213 214 err := m.UnmarshalJSON(data) 215 if err != nil { 216 return err 217 } 218 219 return x.readFromV2(m, false, r) 220 } 221 222 // SetExp sets "exp" (expiration time) claim which identifies the expiration 223 // time (in FrostFS epochs) after which the session MUST NOT be accepted for 224 // processing. The processing of the "exp" claim requires that the current 225 // epoch MUST be before or equal to the expiration epoch listed in the "exp" 226 // claim. 227 // 228 // Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4. 229 // 230 // See also ExpiredAt. 231 func (x *commonData) SetExp(exp uint64) { 232 x.exp = exp 233 x.lifetimeSet = true 234 } 235 236 // SetNbf sets "nbf" (not before) claim which identifies the time (in FrostFS 237 // epochs) before which the session MUST NOT be accepted for processing. 238 // The processing of the "nbf" claim requires that the current date/time MUST be 239 // after or equal to the not-before date/time listed in the "nbf" claim. 240 // 241 // Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5. 242 // 243 // See also InvalidAt. 244 func (x *commonData) SetNbf(nbf uint64) { 245 x.nbf = nbf 246 x.lifetimeSet = true 247 } 248 249 // SetIat sets "iat" (issued at) claim which identifies the time (in FrostFS 250 // epochs) at which the session was issued. This claim can be used to 251 // determine the age of the session. 252 // 253 // Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6. 254 // 255 // See also InvalidAt. 256 func (x *commonData) SetIat(iat uint64) { 257 x.iat = iat 258 x.lifetimeSet = true 259 } 260 261 func (x commonData) expiredAt(epoch uint64) bool { 262 return !x.lifetimeSet || x.exp < epoch 263 } 264 265 // InvalidAt asserts "exp", "nbf" and "iat" claims. 266 // 267 // Zero session is invalid in any epoch. 268 // 269 // See also SetExp, SetNbf, SetIat. 270 func (x commonData) InvalidAt(epoch uint64) bool { 271 return x.expiredAt(epoch) || x.nbf > epoch || x.iat > epoch 272 } 273 274 // SetID sets a unique identifier for the session. The identifier value MUST be 275 // assigned in a manner that ensures that there is a negligible probability 276 // that the same value will be accidentally assigned to a different session. 277 // 278 // ID format MUST be UUID version 4 (random). uuid.New can be used to generate 279 // a new ID. See https://datatracker.ietf.org/doc/html/rfc4122 and 280 // github.com/google/uuid package docs for details. 281 // 282 // See also ID. 283 func (x *commonData) SetID(id uuid.UUID) { 284 x.id = id 285 x.idSet = true 286 } 287 288 // ID returns a unique identifier for the session. 289 // 290 // Zero session has empty UUID (all zeros, see uuid.Nil) which is legitimate 291 // but most likely not suitable. 292 // 293 // See also SetID. 294 func (x commonData) ID() uuid.UUID { 295 if x.idSet { 296 return x.id 297 } 298 299 return uuid.Nil 300 } 301 302 // SetAuthKey public key corresponding to the private key bound to the session. 303 // 304 // See also AssertAuthKey. 305 func (x *commonData) SetAuthKey(key frostfscrypto.PublicKey) { 306 x.authKey = make([]byte, key.MaxEncodedSize()) 307 x.authKey = x.authKey[:key.Encode(x.authKey)] 308 } 309 310 // AssertAuthKey asserts public key bound to the session. 311 // 312 // Zero session fails the check. 313 // 314 // See also SetAuthKey. 315 func (x commonData) AssertAuthKey(key frostfscrypto.PublicKey) bool { 316 bKey := make([]byte, key.MaxEncodedSize()) 317 bKey = bKey[:key.Encode(bKey)] 318 319 return bytes.Equal(bKey, x.authKey) 320 } 321 322 // Issuer returns user ID of the session issuer. 323 // 324 // Makes sense only for signed session instances. For unsigned instances, 325 // Issuer returns zero user.ID. 326 // 327 // See also Sign. 328 func (x commonData) Issuer() user.ID { 329 if x.issuerSet { 330 return x.issuer 331 } 332 333 return user.ID{} 334 }