c65be6d469
Sync lifetime names with `session` package. Add `AssertContainer` and `AssertUser` methods which cover specific cases describe in protocol. Increase test coverage. Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
370 lines
9 KiB
Go
370 lines
9 KiB
Go
package bearer
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/nspcc-dev/neofs-api-go/v2/acl"
|
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
|
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
|
|
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
|
|
"github.com/nspcc-dev/neofs-sdk-go/eacl"
|
|
"github.com/nspcc-dev/neofs-sdk-go/user"
|
|
)
|
|
|
|
// Token represents bearer token for object service operations.
|
|
//
|
|
// Token is mutually compatible with github.com/nspcc-dev/neofs-api-go/v2/acl.BearerToken
|
|
// message. See ReadFromV2 / WriteToV2 methods.
|
|
//
|
|
// Instances can be created using built-in var declaration.
|
|
type Token struct {
|
|
targetUserSet bool
|
|
targetUser user.ID
|
|
|
|
eaclTableSet bool
|
|
eaclTable eacl.Table
|
|
|
|
lifetimeSet bool
|
|
iat, nbf, exp uint64
|
|
|
|
sigSet bool
|
|
sig refs.Signature
|
|
}
|
|
|
|
// reads Token from the acl.BearerToken message. If checkFieldPresence is set,
|
|
// returns an error on absence of any protocol-required field.
|
|
func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error {
|
|
var err error
|
|
|
|
body := m.GetBody()
|
|
if checkFieldPresence && body == nil {
|
|
return errors.New("missing token body")
|
|
}
|
|
|
|
eaclTable := body.GetEACL()
|
|
if b.eaclTableSet = eaclTable != nil; b.eaclTableSet {
|
|
b.eaclTable = *eacl.NewTableFromV2(eaclTable)
|
|
} else if checkFieldPresence {
|
|
return errors.New("missing eACL table")
|
|
}
|
|
|
|
targetUser := body.GetOwnerID()
|
|
if b.targetUserSet = targetUser != nil; b.targetUserSet {
|
|
err = b.targetUser.ReadFromV2(*targetUser)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid target user: %w", err)
|
|
}
|
|
}
|
|
|
|
lifetime := body.GetLifetime()
|
|
if b.lifetimeSet = lifetime != nil; b.lifetimeSet {
|
|
b.iat = lifetime.GetIat()
|
|
b.nbf = lifetime.GetNbf()
|
|
b.exp = lifetime.GetExp()
|
|
} else if checkFieldPresence {
|
|
return errors.New("missing token lifetime")
|
|
}
|
|
|
|
sig := m.GetSignature()
|
|
if b.sigSet = sig != nil; sig != nil {
|
|
b.sig = *sig
|
|
} else if checkFieldPresence {
|
|
return errors.New("missing body signature")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadFromV2 reads Token from the acl.BearerToken message.
|
|
//
|
|
// See also WriteToV2.
|
|
func (b *Token) ReadFromV2(m acl.BearerToken) error {
|
|
return b.readFromV2(m, true)
|
|
}
|
|
|
|
func (b Token) fillBody() *acl.BearerTokenBody {
|
|
if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet {
|
|
return nil
|
|
}
|
|
|
|
var body acl.BearerTokenBody
|
|
|
|
if b.eaclTableSet {
|
|
body.SetEACL(b.eaclTable.ToV2())
|
|
}
|
|
|
|
if b.targetUserSet {
|
|
var targetUser refs.OwnerID
|
|
b.targetUser.WriteToV2(&targetUser)
|
|
|
|
body.SetOwnerID(&targetUser)
|
|
}
|
|
|
|
if b.lifetimeSet {
|
|
var lifetime acl.TokenLifetime
|
|
lifetime.SetIat(b.iat)
|
|
lifetime.SetNbf(b.nbf)
|
|
lifetime.SetExp(b.exp)
|
|
|
|
body.SetLifetime(&lifetime)
|
|
}
|
|
|
|
return &body
|
|
}
|
|
|
|
func (b Token) signedData() []byte {
|
|
return b.fillBody().StableMarshal(nil)
|
|
}
|
|
|
|
// WriteToV2 writes Token to the acl.BearerToken message.
|
|
// The message must not be nil.
|
|
//
|
|
// See also ReadFromV2.
|
|
func (b Token) WriteToV2(m *acl.BearerToken) {
|
|
m.SetBody(b.fillBody())
|
|
|
|
var sig *refs.Signature
|
|
|
|
if b.sigSet {
|
|
sig = &b.sig
|
|
}
|
|
|
|
m.SetSignature(sig)
|
|
}
|
|
|
|
// SetExp sets "exp" (expiration time) claim which identifies the
|
|
// expiration time (in NeoFS epochs) on or after which the Token MUST NOT be
|
|
// accepted for processing. The processing of the "exp" claim requires that the
|
|
// current epoch MUST be before 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 InvalidAt.
|
|
func (b *Token) SetExp(exp uint64) {
|
|
b.exp = exp
|
|
b.lifetimeSet = true
|
|
}
|
|
|
|
// SetNbf sets "nbf" (not before) claim which identifies the time (in
|
|
// NeoFS epochs) before which the Token MUST NOT be accepted for processing. The
|
|
// processing of the "nbf" claim requires that the current epoch MUST be
|
|
// after or equal to the not-before epoch listed in the "nbf" claim.
|
|
//
|
|
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5.
|
|
//
|
|
// See also InvalidAt.
|
|
func (b *Token) SetNbf(nbf uint64) {
|
|
b.nbf = nbf
|
|
b.lifetimeSet = true
|
|
}
|
|
|
|
// SetIat sets "iat" (issued at) claim which identifies the time (in NeoFS
|
|
// epochs) at which the Token was issued. This claim can be used to determine
|
|
// the age of the Token.
|
|
//
|
|
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6.
|
|
//
|
|
// See also InvalidAt.
|
|
func (b *Token) SetIat(iat uint64) {
|
|
b.iat = iat
|
|
b.lifetimeSet = true
|
|
}
|
|
|
|
// InvalidAt asserts "exp", "nbf" and "iat" claims for the given epoch.
|
|
//
|
|
// Zero Container is invalid in any epoch.
|
|
//
|
|
// See also SetExp, SetNbf, SetIat.
|
|
func (b Token) InvalidAt(epoch uint64) bool {
|
|
return !b.lifetimeSet || b.nbf > epoch || b.iat > epoch || b.exp <= epoch
|
|
}
|
|
|
|
// SetEACLTable sets eacl.Table that replaces the one from the issuer's
|
|
// container. If table has specified container, bearer token can be used only
|
|
// for operations within this specific container. Otherwise, Token can be used
|
|
// within any issuer's container.
|
|
//
|
|
// SetEACLTable MUST be called if Token is going to be transmitted over
|
|
// NeoFS API V2 protocol.
|
|
//
|
|
// See also EACLTable, AssertContainer.
|
|
func (b *Token) SetEACLTable(table eacl.Table) {
|
|
b.eaclTable = table
|
|
b.eaclTableSet = true
|
|
}
|
|
|
|
// EACLTable returns extended ACL table set by SetEACLTable.
|
|
//
|
|
// Zero Token has zero eacl.Table.
|
|
func (b Token) EACLTable() eacl.Table {
|
|
if b.eaclTableSet {
|
|
return b.eaclTable
|
|
}
|
|
|
|
return eacl.Table{}
|
|
}
|
|
|
|
// AssertContainer checks if the token is valid within the given container.
|
|
//
|
|
// Note: cnr is assumed to refer to the issuer's container, otherwise the check
|
|
// is meaningless.
|
|
//
|
|
// Zero Token is valid in any container.
|
|
//
|
|
// See also SetEACLTable.
|
|
func (b Token) AssertContainer(cnr cid.ID) bool {
|
|
if !b.eaclTableSet {
|
|
return true
|
|
}
|
|
|
|
cnrTable, set := b.eaclTable.CID()
|
|
return !set || cnrTable.Equals(cnr)
|
|
}
|
|
|
|
// ForUser specifies ID of the user who can use the Token for the operations
|
|
// within issuer's container(s).
|
|
//
|
|
// Optional: by default, any user has access to Token usage.
|
|
//
|
|
// See also AssertUser.
|
|
func (b *Token) ForUser(id user.ID) {
|
|
b.targetUser = id
|
|
b.targetUserSet = true
|
|
}
|
|
|
|
// AssertUser checks if the Token is issued to the given user.
|
|
//
|
|
// Zero Token is available to any user.
|
|
//
|
|
// See also ForUser.
|
|
func (b Token) AssertUser(id user.ID) bool {
|
|
return !b.targetUserSet || b.targetUser.Equals(id)
|
|
}
|
|
|
|
// Sign calculates and writes signature of the Token data using issuer's secret.
|
|
// Returns signature calculation errors.
|
|
//
|
|
// Sign MUST be called if Token is going to be transmitted over
|
|
// NeoFS API V2 protocol.
|
|
//
|
|
// Note that any Token mutation is likely to break the signature, so it is
|
|
// expected to be calculated as a final stage of Token formation.
|
|
//
|
|
// See also VerifySignature, Issuer.
|
|
func (b *Token) Sign(key ecdsa.PrivateKey) error {
|
|
var sig neofscrypto.Signature
|
|
|
|
err := sig.Calculate(neofsecdsa.Signer(key), b.signedData())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sig.WriteToV2(&b.sig)
|
|
b.sigSet = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifySignature checks if Token signature is presented and valid.
|
|
//
|
|
// Zero Token fails the check.
|
|
//
|
|
// See also Sign.
|
|
func (b Token) VerifySignature() bool {
|
|
if !b.sigSet {
|
|
return false
|
|
}
|
|
|
|
var sig neofscrypto.Signature
|
|
sig.ReadFromV2(b.sig)
|
|
|
|
// TODO: (#233) check owner<->key relation
|
|
return sig.Verify(b.signedData())
|
|
}
|
|
|
|
// Marshal encodes Token into a binary format of the NeoFS API protocol
|
|
// (Protocol Buffers V3 with direct field order).
|
|
//
|
|
// See also Unmarshal.
|
|
func (b Token) Marshal() []byte {
|
|
var m acl.BearerToken
|
|
b.WriteToV2(&m)
|
|
|
|
return m.StableMarshal(nil)
|
|
}
|
|
|
|
// Unmarshal decodes NeoFS API protocol binary data into the Token
|
|
// (Protocol Buffers V3 with direct field order). Returns an error describing
|
|
// a format violation.
|
|
//
|
|
// See also Marshal.
|
|
func (b *Token) Unmarshal(data []byte) error {
|
|
var m acl.BearerToken
|
|
|
|
err := m.Unmarshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.readFromV2(m, false)
|
|
}
|
|
|
|
// MarshalJSON encodes Token into a JSON format of the NeoFS API protocol
|
|
// (Protocol Buffers V3 JSON).
|
|
//
|
|
// See also UnmarshalJSON.
|
|
func (b Token) MarshalJSON() ([]byte, error) {
|
|
var m acl.BearerToken
|
|
b.WriteToV2(&m)
|
|
|
|
return m.MarshalJSON()
|
|
}
|
|
|
|
// UnmarshalJSON decodes NeoFS API protocol JSON data into the Token
|
|
// (Protocol Buffers V3 JSON). Returns an error describing a format violation.
|
|
//
|
|
// See also MarshalJSON.
|
|
func (b *Token) UnmarshalJSON(data []byte) error {
|
|
var m acl.BearerToken
|
|
|
|
err := m.UnmarshalJSON(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.readFromV2(m, false)
|
|
}
|
|
|
|
// SigningKeyBytes returns issuer's public key in a binary format of
|
|
// NeoFS API protocol.
|
|
//
|
|
// Unsigned Token has empty key.
|
|
//
|
|
// See also ResolveIssuer.
|
|
func (b Token) SigningKeyBytes() []byte {
|
|
if b.sigSet {
|
|
return b.sig.GetKey()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResolveIssuer resolves issuer's user.ID from the key used for Token signing.
|
|
// Returns zero user.ID if Token is unsigned or key has incorrect format.
|
|
//
|
|
// See also SigningKeyBytes.
|
|
func ResolveIssuer(b Token) (usr user.ID) {
|
|
binKey := b.SigningKeyBytes()
|
|
|
|
if len(binKey) != 0 {
|
|
var key neofsecdsa.PublicKey
|
|
if key.Decode(binKey) == nil {
|
|
user.IDFromKey(&usr, ecdsa.PublicKey(key))
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|