forked from TrueCloudLab/frostfs-sdk-go
334 lines
8 KiB
Go
334 lines
8 KiB
Go
package session
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/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{}
|
|
}
|