diff --git a/service/bearer.go b/service/bearer.go new file mode 100644 index 0000000..bc8aaa5 --- /dev/null +++ b/service/bearer.go @@ -0,0 +1,96 @@ +package service + +import ( + "crypto/ecdsa" + "io" + + "github.com/nspcc-dev/neofs-api-go/refs" + crypto "github.com/nspcc-dev/neofs-crypto" +) + +type signedBearerToken struct { + BearerToken +} + +const fixedBearerTokenDataSize = 0 + + refs.OwnerIDSize + + 8 + +// NewSignedBearerToken wraps passed BearerToken in a component suitable for signing. +// +// Result can be used in AddSignatureWithKey function. +func NewSignedBearerToken(token BearerToken) DataWithSignKeyAccumulator { + return &signedBearerToken{ + BearerToken: token, + } +} + +// NewVerifiedBearerToken wraps passed SessionToken in a component suitable for signature verification. +// +// Result can be used in VerifySignatureWithKey function. +func NewVerifiedBearerToken(token BearerToken) DataWithSignature { + return &signedBearerToken{ + BearerToken: token, + } +} + +// AddSignKey calls a Signature field setter and an OwnerKey field setter with corresponding arguments. +func (s signedBearerToken) AddSignKey(sig []byte, key *ecdsa.PublicKey) { + if s.BearerToken != nil { + s.SetSignature(sig) + + s.SetOwnerKey( + crypto.MarshalPublicKey(key), + ) + } +} + +// SignedData returns token information in a binary representation. +func (s signedBearerToken) SignedData() ([]byte, error) { + return SignedDataFromReader(s) +} + +// SignedDataSize returns the length of signed token information slice. +func (s signedBearerToken) SignedDataSize() int { + return bearerTokenInfoSize(s.BearerToken) +} + +// ReadSignedData copies a binary representation of the token information to passed buffer. +// +// If buffer length is less than required, io.ErrUnexpectedEOF returns. +func (s signedBearerToken) ReadSignedData(p []byte) (int, error) { + sz := s.SignedDataSize() + if len(p) < sz { + return 0, io.ErrUnexpectedEOF + } + + copyBearerTokenSignedData(p, s.BearerToken) + + return sz, nil +} + +func bearerTokenInfoSize(v ACLRulesSource) int { + if v == nil { + return 0 + } + return fixedBearerTokenDataSize + len(v.GetACLRules()) +} + +// Fills passed buffer with signing token information bytes. +// Does not check buffer length, it is understood that enough space is allocated in it. +// +// If passed BearerTokenInfo, buffer remains unchanged. +func copyBearerTokenSignedData(buf []byte, token BearerTokenInfo) { + if token == nil { + return + } + + var off int + + off += copy(buf[off:], token.GetACLRules()) + + off += copy(buf[off:], token.GetOwnerID().Bytes()) + + tokenEndianness.PutUint64(buf[off:], token.ExpirationEpoch()) + off += 8 +} diff --git a/service/bearer_test.go b/service/bearer_test.go new file mode 100644 index 0000000..da359f2 --- /dev/null +++ b/service/bearer_test.go @@ -0,0 +1,172 @@ +package service + +import ( + "crypto/rand" + "testing" + + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +type testBearerToken struct { + aclRules []byte + expEpoch uint64 + owner OwnerID + key []byte + sig []byte +} + +func (s testBearerToken) GetACLRules() []byte { + return s.aclRules +} + +func (s *testBearerToken) SetACLRules(v []byte) { + s.aclRules = v +} + +func (s testBearerToken) ExpirationEpoch() uint64 { + return s.expEpoch +} + +func (s *testBearerToken) SetExpirationEpoch(v uint64) { + s.expEpoch = v +} + +func (s testBearerToken) GetOwnerID() OwnerID { + return s.owner +} + +func (s *testBearerToken) SetOwnerID(v OwnerID) { + s.owner = v +} + +func (s testBearerToken) GetOwnerKey() []byte { + return s.key +} + +func (s *testBearerToken) SetOwnerKey(v []byte) { + s.key = v +} + +func (s testBearerToken) GetSignature() []byte { + return s.sig +} + +func (s *testBearerToken) SetSignature(v []byte) { + s.sig = v +} + +func TestBearerTokenMsgGettersSetters(t *testing.T) { + var tok BearerToken = new(testBearerToken) + + { // ACLRules + rules := []byte{1, 2, 3} + + tok.SetACLRules(rules) + + require.Equal(t, rules, tok.GetACLRules()) + } + + { // OwnerID + ownerID := OwnerID{} + _, err := rand.Read(ownerID[:]) + require.NoError(t, err) + + tok.SetOwnerID(ownerID) + + require.Equal(t, ownerID, tok.GetOwnerID()) + } + + { // ValidUntil + e := uint64(5) + + tok.SetExpirationEpoch(e) + + require.Equal(t, e, tok.ExpirationEpoch()) + } + + { // OwnerKey + key := make([]byte, 10) + _, err := rand.Read(key) + require.NoError(t, err) + + tok.SetOwnerKey(key) + + require.Equal(t, key, tok.GetOwnerKey()) + } + + { // Signature + sig := make([]byte, 10) + _, err := rand.Read(sig) + require.NoError(t, err) + + tok.SetSignature(sig) + + require.Equal(t, sig, tok.GetSignature()) + } +} + +func TestSignVerifyBearerToken(t *testing.T) { + var token BearerToken = new(testBearerToken) + + // create private key for signing + sk := test.DecodeKey(0) + pk := &sk.PublicKey + + rules := []byte{1, 2, 3} + token.SetACLRules(rules) + + ownerID := OwnerID{} + _, err := rand.Read(ownerID[:]) + require.NoError(t, err) + token.SetOwnerID(ownerID) + + fEpoch := uint64(2) + token.SetExpirationEpoch(fEpoch) + + signedToken := NewSignedBearerToken(token) + verifiedToken := NewVerifiedBearerToken(token) + + // sign and verify token + require.NoError(t, AddSignatureWithKey(sk, signedToken)) + require.NoError(t, VerifySignatureWithKey(pk, verifiedToken)) + + items := []struct { + corrupt func() + restore func() + }{ + { // ACLRules + corrupt: func() { + token.SetACLRules(append(rules, 1)) + }, + restore: func() { + token.SetACLRules(rules) + }, + }, + { // Owner ID + corrupt: func() { + ownerID[0]++ + token.SetOwnerID(ownerID) + }, + restore: func() { + ownerID[0]-- + token.SetOwnerID(ownerID) + }, + }, + { // Expiration epoch + corrupt: func() { + token.SetExpirationEpoch(fEpoch + 1) + }, + restore: func() { + token.SetExpirationEpoch(fEpoch) + }, + }, + } + + for _, v := range items { + v.corrupt() + require.Error(t, VerifySignatureWithKey(pk, verifiedToken)) + v.restore() + require.NoError(t, VerifySignatureWithKey(pk, verifiedToken)) + } +} diff --git a/service/types.go b/service/types.go index 257b0ca..dada0c8 100644 --- a/service/types.go +++ b/service/types.go @@ -267,3 +267,37 @@ type RequestVerifyData interface { RequestData SignKeyPairSource } + +// ACLRulesSource is an interface of the container of binary extended ACL rules with read access. +type ACLRulesSource interface { + GetACLRules() []byte +} + +// ACLRulesContainer is an interface of the container of binary extended ACL rules. +type ACLRulesContainer interface { + ACLRulesSource + SetACLRules([]byte) +} + +// BearerTokenInfo is an interface of a fixed set of Bearer token information value containers. +// Contains: +// - binary extended ACL rules; +// - expiration epoch number; +// - ID of the token's owner. +type BearerTokenInfo interface { + ACLRulesContainer + ExpirationEpochContainer + OwnerIDContainer +} + +// BearerToken is an interface of Bearer token information and key-signature pair. +type BearerToken interface { + BearerTokenInfo + OwnerKeyContainer + SignatureContainer +} + +// BearerTokenSource is an interface of the container of a BearerToken with read access. +type BearerTokenSource interface { + GetBearerToken() BearerToken +}