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  }