Authorization improvements (#925)

This commit is contained in:
Bryan White 2018-12-31 10:45:43 -05:00 committed by GitHub
parent 9a271dabf9
commit 4efb5c0a75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 183 additions and 52 deletions

View File

@ -275,17 +275,17 @@ func writeTokenInfo(claimed, open certificates.Authorizations, w io.Writer) erro
"Open": open,
}
for label, group := range groups {
if _, err := fmt.Fprintf(w, "%s:\n", label); err != nil {
if _, err := fmt.Fprintf(w, "\t%s:\n", label); err != nil {
return err
}
if len(group) > 0 {
for _, auth := range group {
if _, err := fmt.Fprintf(w, "\t%s\n", auth.Token.String()); err != nil {
if _, err := fmt.Fprintf(w, "\t\t%s\n", auth.Token.String()); err != nil {
return err
}
}
} else {
if _, err := fmt.Fprintln(w, "\tnone"); err != nil {
if _, err := fmt.Fprintln(w, "\t\tnone"); err != nil {
return err
}
}
@ -306,7 +306,7 @@ func cmdExportAuth(cmd *cobra.Command, args []string) error {
}
emails = args
} else if authExportCfg.All {
emails, err = authDB.Emails()
emails, err = authDB.UserIDs()
if err != nil {
return err
}

View File

@ -11,6 +11,7 @@ import (
"encoding/gob"
"fmt"
"os"
"strings"
"github.com/btcsuite/btcutil/base58"
"github.com/zeebo/errs"
@ -29,7 +30,9 @@ import (
const (
// AuthorizationsBucket is the bucket used with a bolt-backed authorizations DB.
AuthorizationsBucket = "authorizations"
tokenLength = 64 // 2^(64*8) =~ 1.34E+154
tokenDataLength = 64 // 2^(64*8) =~ 1.34E+154
tokenDelimiter = ":"
tokenVersion = 0
)
var (
@ -38,6 +41,8 @@ var (
ErrAuthorization = errs.Class("authorization error")
// ErrAuthorizationDB is used when an error occurs involving the authorization database.
ErrAuthorizationDB = errs.Class("authorization db error")
// ErrInvalidToken is used when a token is invalid
ErrInvalidToken = errs.Class("invalid token error")
// ErrAuthorizationCount is used when attempting to create an invalid number of authorizations.
ErrAuthorizationCount = ErrAuthorizationDB.New("cannot add less than one authorizations")
)
@ -69,9 +74,13 @@ type Authorization struct {
Claim *Claim
}
// Token is a random byte array to be used like a pre-shared key for claiming
// certificate signatures.
type Token [tokenLength]byte
// Token is a userID and a random byte array, when serialized, can be used like
// a pre-shared key for claiming certificate signatures.
type Token struct {
// NB: currently email address for convenience
UserID string
Data [tokenDataLength]byte
}
// Claim holds information about the circumstances under which an authorization
// token was claimed.
@ -92,9 +101,9 @@ func NewServer(log *zap.Logger) pb.CertificatesServer {
}
// NewAuthorization creates a new, unclaimed authorization with a random token value
func NewAuthorization() (*Authorization, error) {
var token Token
_, err := rand.Read(token[:])
func NewAuthorization(userID string) (*Authorization, error) {
token := Token{UserID: userID}
_, err := rand.Read(token.Data[:])
if err != nil {
return nil, ErrAuthorization.Wrap(err)
}
@ -104,6 +113,34 @@ func NewAuthorization() (*Authorization, error) {
}, nil
}
// ParseToken splits the token string on the delimiter to get a userID and data
// for a token and base58 decodes the data.
func ParseToken(tokenString string) (*Token, error) {
splitAt := strings.LastIndex(tokenString, tokenDelimiter)
if splitAt == -1 {
return nil, ErrInvalidToken.New("delimiter missing")
}
userID, b58Data := tokenString[:splitAt], tokenString[splitAt+1:]
if len(userID) == 0 {
return nil, ErrInvalidToken.New("user ID missing")
}
data, _, err := base58.CheckDecode(b58Data)
if err != nil {
return nil, ErrInvalidToken.Wrap(err)
}
if len(data) != tokenDataLength {
return nil, ErrInvalidToken.New("data size mismatch")
}
t := &Token{
UserID: userID,
}
copy(t.Data[:], data)
return t, nil
}
// NewAuthDB creates or opens the authorization database specified by the config
func (c CertSignerConfig) NewAuthDB() (*AuthorizationDB, error) {
// TODO: refactor db selection logic?
@ -169,15 +206,15 @@ func (a *AuthorizationDB) Close() error {
}
// Create creates a new authorization and adds it to the authorization database.
func (a *AuthorizationDB) Create(email string, count int) (Authorizations, error) {
if len(email) == 0 {
return nil, ErrAuthorizationDB.New("email cannot be empty")
func (a *AuthorizationDB) Create(userID string, count int) (Authorizations, error) {
if len(userID) == 0 {
return nil, ErrAuthorizationDB.New("userID cannot be empty")
}
if count < 1 {
return nil, ErrAuthorizationCount
}
existingAuths, err := a.Get(email)
existingAuths, err := a.Get(userID)
if err != nil {
return nil, err
}
@ -187,7 +224,7 @@ func (a *AuthorizationDB) Create(email string, count int) (Authorizations, error
authErrs utils.ErrorGroup
)
for i := 0; i < count; i++ {
auth, err := NewAuthorization()
auth, err := NewAuthorization(userID)
if err != nil {
authErrs.Add(err)
continue
@ -204,7 +241,7 @@ func (a *AuthorizationDB) Create(email string, count int) (Authorizations, error
return nil, ErrAuthorizationDB.Wrap(err)
}
if err := a.DB.Put(storage.Key(email), authsBytes); err != nil {
if err := a.DB.Put(storage.Key(userID), authsBytes); err != nil {
return nil, ErrAuthorizationDB.Wrap(err)
}
@ -228,8 +265,8 @@ func (a *AuthorizationDB) Get(email string) (Authorizations, error) {
return auths, nil
}
// Emails returns a list of all emails present in the authorization database.
func (a *AuthorizationDB) Emails() ([]string, error) {
// UserIDs returns a list of all userIDs present in the authorization database.
func (a *AuthorizationDB) UserIDs() ([]string, error) {
keys, err := a.DB.List([]byte{}, 0)
if err != nil {
return nil, ErrAuthorizationDB.Wrap(err)
@ -280,5 +317,5 @@ func (a Authorization) String() string {
// String implements the stringer interface. Base68 w/ version and checksum bytes
// are used for easy and reliable human transport.
func (t *Token) String() string {
return base58.CheckEncode(t[:], 0)
return fmt.Sprintf("%s:%s", t.UserID, base58.CheckEncode(t.Data[:], tokenVersion))
}

View File

@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/btcsuite/btcutil/base58"
"github.com/stretchr/testify/assert"
"github.com/zeebo/errs"
@ -19,6 +20,17 @@ import (
"storj.io/storj/storage"
)
var (
t1 = Token{
UserID: "user@example.com",
Data: [tokenDataLength]byte{1, 2, 3},
}
t2 = Token{
UserID: "user2@example.com",
Data: [tokenDataLength]byte{4, 5, 6},
}
)
func TestCertSignerConfig_NewAuthDB(t *testing.T) {
ctx := testcontext.New(t)
authDB, err := newTestAuthDB(ctx)
@ -139,7 +151,7 @@ func TestAuthorizationDB_Get(t *testing.T) {
var expectedAuths Authorizations
for i := 0; i < 5; i++ {
expectedAuths = append(expectedAuths, &Authorization{
Token: [tokenLength]byte{1, 2, 3},
Token: t1,
})
}
authsBytes, err := expectedAuths.Marshal()
@ -183,15 +195,18 @@ func TestAuthorizationDB_Get(t *testing.T) {
}
func TestNewAuthorization(t *testing.T) {
auth, err := NewAuthorization()
userID := "user@example.com"
auth, err := NewAuthorization(userID)
assert.NoError(t, err)
assert.NotNil(t, auth)
if !assert.NotNil(t, auth) {
t.FailNow()
}
assert.NotZero(t, auth.Token)
assert.Equal(t, userID, auth.Token.UserID)
assert.NotEmpty(t, auth.Token.Data)
}
func TestAuthorizations_Marshal(t *testing.T) {
t1 := [tokenLength]byte{1, 2, 3}
t2 := [tokenLength]byte{4, 5, 6}
expectedAuths := Authorizations{
{Token: t1},
{Token: t2},
@ -210,8 +225,6 @@ func TestAuthorizations_Marshal(t *testing.T) {
}
func TestAuthorizations_Unmarshal(t *testing.T) {
t1 := [tokenLength]byte{1, 2, 3}
t2 := [tokenLength]byte{4, 5, 6}
expectedAuths := Authorizations{
{Token: t1},
{Token: t2},
@ -229,8 +242,6 @@ func TestAuthorizations_Unmarshal(t *testing.T) {
}
func TestAuthorizations_Group(t *testing.T) {
t1 := [tokenLength]byte{1, 2, 3}
t2 := [tokenLength]byte{4, 5, 6}
auths := make(Authorizations, 10)
for i := 0; i < 10; i++ {
if i%2 == 0 {
@ -279,9 +290,82 @@ func TestAuthorizationDB_Emails(t *testing.T) {
t.Fatal(err)
}
emails, err := authDB.Emails()
userIDs, err := authDB.UserIDs()
assert.NoError(t, err)
assert.NotEmpty(t, emails)
assert.NotEmpty(t, userIDs)
}
func TestParseToken_Valid(t *testing.T) {
userID := "user@example.com"
data := [tokenDataLength]byte{1, 2, 3}
cases := []struct {
testID string
userID string
}{
{
"valid token",
userID,
},
{
"multiple delimiters",
"us" + tokenDelimiter + "er@example.com",
},
}
for _, c := range cases {
t.Run(c.testID, func(t *testing.T) {
b58Data := base58.CheckEncode(data[:], tokenVersion)
tokenString := c.userID + tokenDelimiter + b58Data
token, err := ParseToken(tokenString)
assert.NoError(t, err)
if !assert.NotNil(t, token) {
t.FailNow()
}
assert.Equal(t, c.userID, token.UserID)
assert.Equal(t, data[:], token.Data[:])
})
}
}
func TestParseToken_Invalid(t *testing.T) {
userID := "user@example.com"
data := [tokenDataLength]byte{1, 2, 3}
cases := []struct {
testID string
tokenString string
}{
{
"no delimiter",
userID + base58.CheckEncode(data[:], tokenVersion),
},
{
"missing userID",
tokenDelimiter + base58.CheckEncode(data[:], tokenVersion),
},
{
"not enough data",
userID + tokenDelimiter + base58.CheckEncode(data[:len(data)-10], tokenVersion),
},
{
"too much data",
userID + tokenDelimiter + base58.CheckEncode(append(data[:], []byte{0, 0, 0}...), tokenVersion),
},
{
"data checksum/format error",
userID + tokenDelimiter + base58.CheckEncode(data[:], tokenVersion)[:len(base58.CheckEncode(data[:], tokenVersion))-4] + "0000",
},
}
for _, c := range cases {
t.Run(c.testID, func(t *testing.T) {
token, err := ParseToken(c.tokenString)
assert.Nil(t, token)
assert.True(t, ErrInvalidToken.Has(err))
})
}
}
func newTestAuthDB(ctx *testcontext.Context) (*AuthorizationDB, error) {

View File

@ -25,7 +25,8 @@ var _ = math.Inf
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type SigningRequest struct {
Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"`
AuthToken string `protobuf:"bytes,1,opt,name=auth_token,json=authToken,proto3" json:"auth_token,omitempty"`
Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -35,7 +36,7 @@ func (m *SigningRequest) Reset() { *m = SigningRequest{} }
func (m *SigningRequest) String() string { return proto.CompactTextString(m) }
func (*SigningRequest) ProtoMessage() {}
func (*SigningRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_certificate_6eecc687f39755aa, []int{0}
return fileDescriptor_certificate_80970fe789528feb, []int{0}
}
func (m *SigningRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SigningRequest.Unmarshal(m, b)
@ -55,15 +56,22 @@ func (m *SigningRequest) XXX_DiscardUnknown() {
var xxx_messageInfo_SigningRequest proto.InternalMessageInfo
func (m *SigningRequest) GetCert() []byte {
func (m *SigningRequest) GetAuthToken() string {
if m != nil {
return m.Cert
return m.AuthToken
}
return nil
return ""
}
func (m *SigningRequest) GetTimestamp() int64 {
if m != nil {
return m.Timestamp
}
return 0
}
type SigningResponse struct {
Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"`
Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -73,7 +81,7 @@ func (m *SigningResponse) Reset() { *m = SigningResponse{} }
func (m *SigningResponse) String() string { return proto.CompactTextString(m) }
func (*SigningResponse) ProtoMessage() {}
func (*SigningResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_certificate_6eecc687f39755aa, []int{1}
return fileDescriptor_certificate_80970fe789528feb, []int{1}
}
func (m *SigningResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SigningResponse.Unmarshal(m, b)
@ -93,9 +101,9 @@ func (m *SigningResponse) XXX_DiscardUnknown() {
var xxx_messageInfo_SigningResponse proto.InternalMessageInfo
func (m *SigningResponse) GetSignature() []byte {
func (m *SigningResponse) GetCert() []byte {
if m != nil {
return m.Signature
return m.Cert
}
return nil
}
@ -177,18 +185,20 @@ var _Certificates_serviceDesc = grpc.ServiceDesc{
Metadata: "certificate.proto",
}
func init() { proto.RegisterFile("certificate.proto", fileDescriptor_certificate_6eecc687f39755aa) }
func init() { proto.RegisterFile("certificate.proto", fileDescriptor_certificate_80970fe789528feb) }
var fileDescriptor_certificate_6eecc687f39755aa = []byte{
// 159 bytes of a gzipped FileDescriptorProto
var fileDescriptor_certificate_80970fe789528feb = []byte{
// 188 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4c, 0x4e, 0x2d, 0x2a,
0xc9, 0x4c, 0xcb, 0x4c, 0x4e, 0x2c, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9,
0xcb, 0x4f, 0x49, 0x95, 0xe2, 0x4a, 0xcf, 0x4f, 0xcf, 0x87, 0x88, 0x28, 0xa9, 0x70, 0xf1, 0x05,
0x67, 0xa6, 0xe7, 0x65, 0xe6, 0xa5, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x09, 0x71,
0xb1, 0x80, 0x34, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, 0x81, 0xd9, 0x4a, 0xfa, 0x5c, 0xfc,
0x70, 0x55, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x32, 0x5c, 0x9c, 0xc5, 0x99, 0xe9, 0x79,
0x89, 0x25, 0xa5, 0x45, 0xa9, 0x50, 0xb5, 0x08, 0x01, 0x23, 0x67, 0x2e, 0x1e, 0x67, 0x84, 0xed,
0xc5, 0x42, 0xc6, 0x5c, 0x2c, 0x20, 0x03, 0x84, 0x44, 0xf4, 0x40, 0x2e, 0xd0, 0x43, 0xb5, 0x52,
0x4a, 0x14, 0x4d, 0x14, 0x62, 0x85, 0x13, 0x4b, 0x14, 0x53, 0x41, 0x52, 0x12, 0x1b, 0xd8, 0xa1,
0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x1b, 0x86, 0x3c, 0x8d, 0xcf, 0x00, 0x00, 0x00,
0xcb, 0x4f, 0x49, 0x95, 0xe2, 0x4a, 0xcf, 0x4f, 0xcf, 0x87, 0x88, 0x28, 0xf9, 0x72, 0xf1, 0x05,
0x67, 0xa6, 0xe7, 0x65, 0xe6, 0xa5, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0xc9, 0x72,
0x71, 0x25, 0x96, 0x96, 0x64, 0xc4, 0x97, 0xe4, 0x67, 0xa7, 0xe6, 0x49, 0x30, 0x2a, 0x30, 0x6a,
0x70, 0x06, 0x71, 0x82, 0x44, 0x42, 0x40, 0x02, 0x42, 0x32, 0x5c, 0x9c, 0x25, 0x99, 0xb9, 0xa9,
0xc5, 0x25, 0x89, 0xb9, 0x05, 0x12, 0x4c, 0x0a, 0x8c, 0x1a, 0xcc, 0x41, 0x08, 0x01, 0x25, 0x55,
0x2e, 0x7e, 0xb8, 0x71, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x42, 0x5c, 0x2c, 0x20, 0x87,
0x80, 0x4d, 0xe2, 0x09, 0x02, 0xb3, 0x8d, 0x9c, 0xb9, 0x78, 0x9c, 0x11, 0x8e, 0x2b, 0x16, 0x32,
0xe6, 0x62, 0x01, 0x69, 0x13, 0x12, 0xd1, 0x03, 0x39, 0x50, 0x0f, 0xd5, 0x45, 0x52, 0xa2, 0x68,
0xa2, 0x10, 0x83, 0x9d, 0x58, 0xa2, 0x98, 0x0a, 0x92, 0x92, 0xd8, 0xc0, 0xfe, 0x30, 0x06, 0x04,
0x00, 0x00, 0xff, 0xff, 0x4b, 0x1b, 0x87, 0x66, 0xee, 0x00, 0x00, 0x00,
}

View File

@ -13,7 +13,7 @@ service Certificates {
}
message SigningRequest {
bytes auth_token = 1;
string auth_token = 1;
int64 timestamp = 2;
}