frostfs-sdk-go/session/common.go

335 lines
8.0 KiB
Go

package session
import (
"bytes"
"crypto/ecdsa"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/google/uuid"
)
type commonData struct {
idSet bool
id uuid.UUID
issuerSet bool
issuer user.ID
lifetimeSet bool
iat, nbf, exp uint64
authKey []byte
sigSet bool
sig refs.Signature
}
type contextReader func(session.TokenContext, bool) error
// reads commonData and custom context from the session.Token message.
// If checkFieldPresence is set, returns an error on absence of any protocol-required
// field. Verifies format of any presented field according to FrostFS API V2 protocol.
// Calls contextReader if session context is set. Passes checkFieldPresence into contextReader.
func (x *commonData) readFromV2(m session.Token, checkFieldPresence bool, r contextReader) error {
var err error
body := m.GetBody()
if checkFieldPresence && body == nil {
return errors.New("missing token body")
}
binID := body.GetID()
if x.idSet = len(binID) > 0; x.idSet {
err = x.id.UnmarshalBinary(binID)
if err != nil {
return fmt.Errorf("invalid session ID: %w", err)
} else if ver := x.id.Version(); ver != 4 {
return fmt.Errorf("invalid session UUID version %d", ver)
}
} else if checkFieldPresence {
return errors.New("missing session ID")
}
issuer := body.GetOwnerID()
if x.issuerSet = issuer != nil; x.issuerSet {
err = x.issuer.ReadFromV2(*issuer)
if err != nil {
return fmt.Errorf("invalid session issuer: %w", err)
}
} else if checkFieldPresence {
return errors.New("missing session issuer")
}
lifetime := body.GetLifetime()
if x.lifetimeSet = lifetime != nil; x.lifetimeSet {
x.iat = lifetime.GetIat()
x.nbf = lifetime.GetNbf()
x.exp = lifetime.GetExp()
} else if checkFieldPresence {
return errors.New("missing token lifetime")
}
x.authKey = body.GetSessionKey()
if checkFieldPresence && len(x.authKey) == 0 {
return errors.New("missing session public key")
}
c := body.GetContext()
if c != nil {
err = r(c, checkFieldPresence)
if err != nil {
return fmt.Errorf("invalid context: %w", err)
}
} else if checkFieldPresence {
return errors.New("missing session context")
}
sig := m.GetSignature()
if x.sigSet = sig != nil; sig != nil {
x.sig = *sig
} else if checkFieldPresence {
return errors.New("missing body signature")
}
return nil
}
type contextWriter func() session.TokenContext
func (x commonData) fillBody(w contextWriter) *session.TokenBody {
var body session.TokenBody
if x.idSet {
binID, err := x.id.MarshalBinary()
if err != nil {
panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err))
}
body.SetID(binID)
}
if x.issuerSet {
var issuer refs.OwnerID
x.issuer.WriteToV2(&issuer)
body.SetOwnerID(&issuer)
}
if x.lifetimeSet {
var lifetime session.TokenLifetime
lifetime.SetIat(x.iat)
lifetime.SetNbf(x.nbf)
lifetime.SetExp(x.exp)
body.SetLifetime(&lifetime)
}
body.SetSessionKey(x.authKey)
body.SetContext(w())
return &body
}
func (x commonData) writeToV2(m *session.Token, w contextWriter) {
body := x.fillBody(w)
m.SetBody(body)
var sig *refs.Signature
if x.sigSet {
sig = &x.sig
}
m.SetSignature(sig)
}
func (x commonData) signedData(w contextWriter) []byte {
return x.fillBody(w).StableMarshal(nil)
}
func (x *commonData) sign(key ecdsa.PrivateKey, w contextWriter) error {
user.IDFromKey(&x.issuer, key.PublicKey)
x.issuerSet = true
var sig frostfscrypto.Signature
err := sig.Calculate(frostfsecdsa.Signer(key), x.signedData(w))
if err != nil {
return err
}
sig.WriteToV2(&x.sig)
x.sigSet = true
return nil
}
func (x commonData) verifySignature(w contextWriter) bool {
if !x.sigSet {
return false
}
var sig frostfscrypto.Signature
// TODO: (#233) check owner<->key relation
return sig.ReadFromV2(x.sig) == nil && sig.Verify(x.signedData(w))
}
func (x commonData) marshal(w contextWriter) []byte {
var m session.Token
x.writeToV2(&m, w)
return m.StableMarshal(nil)
}
func (x *commonData) unmarshal(data []byte, r contextReader) error {
var m session.Token
err := m.Unmarshal(data)
if err != nil {
return err
}
return x.readFromV2(m, false, r)
}
func (x commonData) marshalJSON(w contextWriter) ([]byte, error) {
var m session.Token
x.writeToV2(&m, w)
return m.MarshalJSON()
}
func (x *commonData) unmarshalJSON(data []byte, r contextReader) error {
var m session.Token
err := m.UnmarshalJSON(data)
if err != nil {
return err
}
return x.readFromV2(m, false, r)
}
// SetExp sets "exp" (expiration time) claim which identifies the expiration
// time (in FrostFS epochs) after which the session MUST NOT be accepted for
// processing. The processing of the "exp" claim requires that the current
// epoch MUST be before or equal to the expiration epoch listed in the "exp"
// claim.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4.
//
// See also ExpiredAt.
func (x *commonData) SetExp(exp uint64) {
x.exp = exp
x.lifetimeSet = true
}
// SetNbf sets "nbf" (not before) claim which identifies the time (in FrostFS
// epochs) before which the session MUST NOT be accepted for processing.
// The processing of the "nbf" claim requires that the current date/time MUST be
// after or equal to the not-before date/time listed in the "nbf" claim.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5.
//
// See also InvalidAt.
func (x *commonData) SetNbf(nbf uint64) {
x.nbf = nbf
x.lifetimeSet = true
}
// SetIat sets "iat" (issued at) claim which identifies the time (in FrostFS
// epochs) at which the session was issued. This claim can be used to
// determine the age of the session.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6.
//
// See also InvalidAt.
func (x *commonData) SetIat(iat uint64) {
x.iat = iat
x.lifetimeSet = true
}
func (x commonData) expiredAt(epoch uint64) bool {
return !x.lifetimeSet || x.exp < epoch
}
// InvalidAt asserts "exp", "nbf" and "iat" claims.
//
// Zero session is invalid in any epoch.
//
// See also SetExp, SetNbf, SetIat.
func (x commonData) InvalidAt(epoch uint64) bool {
return x.expiredAt(epoch) || x.nbf > epoch || x.iat > epoch
}
// SetID sets a unique identifier for the session. The identifier value MUST be
// assigned in a manner that ensures that there is a negligible probability
// that the same value will be accidentally assigned to a different session.
//
// ID format MUST be UUID version 4 (random). uuid.New can be used to generate
// a new ID. See https://datatracker.ietf.org/doc/html/rfc4122 and
// github.com/google/uuid package docs for details.
//
// See also ID.
func (x *commonData) SetID(id uuid.UUID) {
x.id = id
x.idSet = true
}
// ID returns a unique identifier for the session.
//
// Zero session has empty UUID (all zeros, see uuid.Nil) which is legitimate
// but most likely not suitable.
//
// See also SetID.
func (x commonData) ID() uuid.UUID {
if x.idSet {
return x.id
}
return uuid.Nil
}
// SetAuthKey public key corresponding to the private key bound to the session.
//
// See also AssertAuthKey.
func (x *commonData) SetAuthKey(key frostfscrypto.PublicKey) {
x.authKey = make([]byte, key.MaxEncodedSize())
x.authKey = x.authKey[:key.Encode(x.authKey)]
}
// AssertAuthKey asserts public key bound to the session.
//
// Zero session fails the check.
//
// See also SetAuthKey.
func (x commonData) AssertAuthKey(key frostfscrypto.PublicKey) bool {
bKey := make([]byte, key.MaxEncodedSize())
bKey = bKey[:key.Encode(bKey)]
return bytes.Equal(bKey, x.authKey)
}
// Issuer returns user ID of the session issuer.
//
// Makes sense only for signed session instances. For unsigned instances,
// Issuer returns zero user.ID.
//
// See also Sign.
func (x commonData) Issuer() user.ID {
if x.issuerSet {
return x.issuer
}
return user.ID{}
}