Merge branch 'master' into cloud-identities

This commit is contained in:
Mariano Cano 2019-05-08 17:19:03 -07:00
commit 54570095d4
14 changed files with 318 additions and 87 deletions

4
Gopkg.lock generated
View file

@ -363,7 +363,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:f1f1df1e19d55a1ef1f0a63633191e7d2a99993c0a17f945c0b9ebd16b17871b" digest = "1:5e778214d472b6d2ad4d544d293d1478d9b222db8ffc6079623fbe3e58e1841e"
name = "github.com/smallstep/nosql" name = "github.com/smallstep/nosql"
packages = [ packages = [
".", ".",
@ -373,7 +373,7 @@
"mysql", "mysql",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "5a355c598075a346d9ca9b50ec10e3f86ac66148" revision = "b66b34823456721912ba037126e92414690c07d6"
[[projects]] [[projects]]
branch = "master" branch = "master"

View file

@ -22,7 +22,6 @@ type Authority struct {
intermediateIdentity *x509util.Identity intermediateIdentity *x509util.Identity
validateOnce bool validateOnce bool
certificates *sync.Map certificates *sync.Map
ottMap *sync.Map
startTime time.Time startTime time.Time
provisioners *provisioner.Collection provisioners *provisioner.Collection
db db.AuthDB db db.AuthDB
@ -40,7 +39,6 @@ func New(config *Config) (*Authority, error) {
var a = &Authority{ var a = &Authority{
config: config, config: config,
certificates: new(sync.Map), certificates: new(sync.Map),
ottMap: new(sync.Map),
provisioners: provisioner.NewCollection(config.getAudiences()), provisioners: provisioner.NewCollection(config.getAudiences()),
} }
if err := a.init(); err != nil { if err := a.init(); err != nil {
@ -58,8 +56,8 @@ func (a *Authority) init() error {
var err error var err error
// Initialize step-ca Database if defined in configuration. // Initialize step-ca Database.
// If a.config.DB is nil then a noopDB will be returned. // If a.config.DB is nil then a simple, barebones in memory DB will be used.
if a.db, err = db.New(a.config.DB); err != nil { if a.db, err = db.New(a.config.DB); err != nil {
return err return err
} }

View file

@ -4,18 +4,12 @@ import (
"crypto/x509" "crypto/x509"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/cli/jose" "github.com/smallstep/cli/jose"
) )
type idUsed struct {
UsedAt int64 `json:"ua,omitempty"`
Subject string `json:"sub,omitempty"`
}
// Claims extends jose.Claims with step attributes. // Claims extends jose.Claims with step attributes.
type Claims struct { type Claims struct {
jose.Claims jose.Claims
@ -65,10 +59,12 @@ func (a *Authority) authorizeToken(ott string) (provisioner.Interface, error) {
// Store the token to protect against reuse. // Store the token to protect against reuse.
if reuseKey, err := p.GetTokenID(ott); err == nil { if reuseKey, err := p.GetTokenID(ott); err == nil {
if _, ok := a.ottMap.LoadOrStore(reuseKey, &idUsed{ ok, err := a.db.UseToken(reuseKey, ott)
UsedAt: time.Now().Unix(), if err != nil {
Subject: claims.Subject, return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when checking if token already used"),
}); ok { http.StatusInternalServerError, errContext}
}
if !ok {
return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext} return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext}
} }
} }

View file

@ -117,7 +117,7 @@ func TestAuthority_authorizeToken(t *testing.T) {
http.StatusUnauthorized, context{"ott": raw}}, http.StatusUnauthorized, context{"ott": raw}},
} }
}, },
"ok": func(t *testing.T) *authorizeTest { "ok/simpledb": func(t *testing.T) *authorizeTest {
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "test.smallstep.com", Subject: "test.smallstep.com",
Issuer: validIssuer, Issuer: validIssuer,
@ -133,9 +133,8 @@ func TestAuthority_authorizeToken(t *testing.T) {
ott: raw, ott: raw,
} }
}, },
"fail/token-already-used": func(t *testing.T) *authorizeTest { "fail/simpledb/token-already-used": func(t *testing.T) *authorizeTest {
_a := testAuthority(t) _a := testAuthority(t)
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "test.smallstep.com", Subject: "test.smallstep.com",
Issuer: validIssuer, Issuer: validIssuer,
@ -155,6 +154,79 @@ func TestAuthority_authorizeToken(t *testing.T) {
http.StatusUnauthorized, context{"ott": raw}}, http.StatusUnauthorized, context{"ott": raw}},
} }
}, },
"ok/mockNoSQLDB": func(t *testing.T) *authorizeTest {
_a := testAuthority(t)
_a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
return true, nil
},
}
cl := jwt.Claims{
Subject: "test.smallstep.com",
Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validAudience,
ID: "43",
}
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
return &authorizeTest{
auth: _a,
ott: raw,
}
},
"fail/mockNoSQLDB/error": func(t *testing.T) *authorizeTest {
_a := testAuthority(t)
_a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
return false, errors.New("force")
},
}
cl := jwt.Claims{
Subject: "test.smallstep.com",
Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validAudience,
ID: "43",
}
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
return &authorizeTest{
auth: _a,
ott: raw,
err: &apiError{errors.New("authorizeToken: failed when checking if token already used: force"),
http.StatusInternalServerError, context{"ott": raw}},
}
},
"fail/mockNoSQLDB/token-already-used": func(t *testing.T) *authorizeTest {
_a := testAuthority(t)
_a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
return false, nil
},
}
cl := jwt.Claims{
Subject: "test.smallstep.com",
Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validAudience,
ID: "43",
}
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
return &authorizeTest{
auth: _a,
ott: raw,
err: &apiError{errors.New("authorizeToken: token already used"),
http.StatusUnauthorized, context{"ott": raw}},
}
},
} }
for name, genTestCase := range tests { for name, genTestCase := range tests {

View file

@ -13,6 +13,7 @@ type MockAuthDB struct {
isRevoked func(string) (bool, error) isRevoked func(string) (bool, error)
revoke func(rci *db.RevokedCertificateInfo) error revoke func(rci *db.RevokedCertificateInfo) error
storeCertificate func(crt *x509.Certificate) error storeCertificate func(crt *x509.Certificate) error
useToken func(id, tok string) (bool, error)
shutdown func() error shutdown func() error
} }
@ -33,6 +34,16 @@ func (m *MockAuthDB) IsRevoked(sn string) (bool, error) {
return m.ret1.(bool), m.err return m.ret1.(bool), m.err
} }
func (m *MockAuthDB) UseToken(id, tok string) (bool, error) {
if m.useToken != nil {
return m.useToken(id, tok)
}
if m.ret1 == nil {
return false, m.err
}
return m.ret1.(bool), m.err
}
func (m *MockAuthDB) Revoke(rci *db.RevokedCertificateInfo) error { func (m *MockAuthDB) Revoke(rci *db.RevokedCertificateInfo) error {
if m.revoke != nil { if m.revoke != nil {
return m.revoke(rci) return m.revoke(rci)

View file

@ -592,7 +592,6 @@ func TestRevoke(t *testing.T) {
tests := map[string]func() test{ tests := map[string]func() test{
"error/token/authorizeRevoke error": func() test { "error/token/authorizeRevoke error": func() test {
a := testAuthority(t) a := testAuthority(t)
a.db = new(db.NoopDB)
ctx := getCtx() ctx := getCtx()
ctx["ott"] = "foo" ctx["ott"] = "foo"
return test{ return test{
@ -609,8 +608,6 @@ func TestRevoke(t *testing.T) {
}, },
"error/nil-db": func() test { "error/nil-db": func() test {
a := testAuthority(t) a := testAuthority(t)
a.db = new(db.NoopDB)
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "sn", Subject: "sn",
Issuer: validIssuer, Issuer: validIssuer,
@ -640,7 +637,12 @@ func TestRevoke(t *testing.T) {
}, },
"error/db-revoke": func() test { "error/db-revoke": func() test {
a := testAuthority(t) a := testAuthority(t)
a.db = &MockAuthDB{err: errors.New("force")} a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
return true, nil
},
err: errors.New("force"),
}
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "sn", Subject: "sn",
@ -671,7 +673,12 @@ func TestRevoke(t *testing.T) {
}, },
"error/already-revoked": func() test { "error/already-revoked": func() test {
a := testAuthority(t) a := testAuthority(t)
a.db = &MockAuthDB{err: db.ErrAlreadyExists} a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
return true, nil
},
err: db.ErrAlreadyExists,
}
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "sn", Subject: "sn",
@ -702,7 +709,11 @@ func TestRevoke(t *testing.T) {
}, },
"ok/token": func() test { "ok/token": func() test {
a := testAuthority(t) a := testAuthority(t)
a.db = &MockAuthDB{} a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
return true, nil
},
}
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "sn", Subject: "sn",

View file

@ -189,6 +189,15 @@ func (ca *CA) Reload() error {
logContinue("Reload failed because server could not be replaced.") logContinue("Reload failed because server could not be replaced.")
return errors.Wrap(err, "error reloading server") return errors.Wrap(err, "error reloading server")
} }
// 1. Stop previous renewer
// 2. Replace ca properties
// Do not replace ca.srv
ca.renewer.Stop()
ca.auth = newCA.auth
ca.config = newCA.config
ca.opts = newCA.opts
ca.renewer = newCA.renewer
return nil return nil
} }

View file

@ -10,8 +10,9 @@ import (
) )
var ( var (
revokedCertsTable = []byte("revoked_x509_certs")
certsTable = []byte("x509_certs") certsTable = []byte("x509_certs")
revokedCertsTable = []byte("revoked_x509_certs")
usedOTTTable = []byte("used_ott")
) )
// ErrAlreadyExists can be returned if the DB attempts to set a key that has // ErrAlreadyExists can be returned if the DB attempts to set a key that has
@ -31,6 +32,7 @@ type AuthDB interface {
IsRevoked(sn string) (bool, error) IsRevoked(sn string) (bool, error)
Revoke(rci *RevokedCertificateInfo) error Revoke(rci *RevokedCertificateInfo) error
StoreCertificate(crt *x509.Certificate) error StoreCertificate(crt *x509.Certificate) error
UseToken(id, tok string) (bool, error)
Shutdown() error Shutdown() error
} }
@ -43,7 +45,7 @@ type DB struct {
// New returns a new database client that implements the AuthDB interface. // New returns a new database client that implements the AuthDB interface.
func New(c *Config) (AuthDB, error) { func New(c *Config) (AuthDB, error) {
if c == nil { if c == nil {
return new(NoopDB), nil return newSimpleDB(c)
} }
db, err := nosql.New(c.Type, c.DataSource, nosql.WithDatabase(c.Database), db, err := nosql.New(c.Type, c.DataSource, nosql.WithDatabase(c.Database),
@ -126,6 +128,23 @@ func (db *DB) StoreCertificate(crt *x509.Certificate) error {
return nil return nil
} }
// UseToken returns true if we were able to successfully store the token for
// for the first time, false otherwise.
func (db *DB) UseToken(id, tok string) (bool, error) {
// If the error is `Not Found` then the certificate has not been revoked.
// Any other error should be propagated to the caller.
_, found, err := db.LoadOrStore(usedOTTTable, []byte(id), []byte(tok))
switch {
case err != nil:
return false, errors.Wrapf(err, "error LoadOrStore-ing token %s/%s",
string(usedOTTTable), id)
case found:
return false, nil
default:
return true, nil
}
}
// Shutdown sends a shutdown message to the database. // Shutdown sends a shutdown message to the database.
func (db *DB) Shutdown() error { func (db *DB) Shutdown() error {
if db.isUp { if db.isUp {

View file

@ -20,6 +20,17 @@ type MockNoSQLDB struct {
del func(bucket, key []byte) error del func(bucket, key []byte) error
list func(bucket []byte) ([]*database.Entry, error) list func(bucket []byte) ([]*database.Entry, error)
update func(tx *database.Tx) error update func(tx *database.Tx) error
loadOrStore func(bucket, key, value []byte) ([]byte, bool, error)
}
func (m *MockNoSQLDB) LoadOrStore(bucket, key, value []byte) ([]byte, bool, error) {
if m.get != nil {
return m.loadOrStore(bucket, key, value)
}
if m.ret1 == nil {
return nil, false, m.err
}
return m.ret1.([]byte), m.ret2.(bool), m.err
} }
func (m *MockNoSQLDB) Get(bucket, key []byte) ([]byte, error) { func (m *MockNoSQLDB) Get(bucket, key []byte) ([]byte, error) {
@ -188,3 +199,66 @@ func TestRevoke(t *testing.T) {
}) })
} }
} }
func TestUseToken(t *testing.T) {
type result struct {
err error
ok bool
}
tests := map[string]struct {
id, tok string
db *DB
want result
}{
"fail/force-LoadOrStore-error": {
id: "id",
tok: "token",
db: &DB{&MockNoSQLDB{
loadOrStore: func(bucket, key, value []byte) ([]byte, bool, error) {
return nil, false, errors.New("force")
},
}, true},
want: result{
ok: false,
err: errors.New("error LoadOrStore-ing token id/token"),
},
},
"fail/LoadOrStore-found": {
id: "id",
tok: "token",
db: &DB{&MockNoSQLDB{
loadOrStore: func(bucket, key, value []byte) ([]byte, bool, error) {
return []byte("foo"), true, nil
},
}, true},
want: result{
ok: false,
},
},
"ok/LoadOrStore-not-found": {
id: "id",
tok: "token",
db: &DB{&MockNoSQLDB{
loadOrStore: func(bucket, key, value []byte) ([]byte, bool, error) {
return nil, false, nil
},
}, true},
want: result{
ok: true,
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ok, err := tc.db.UseToken(tc.id, tc.tok)
if err != nil {
if assert.NotNil(t, tc.want.err) {
assert.HasPrefix(t, tc.want.err.Error(), err.Error())
}
assert.False(t, ok)
} else {
assert.True(t, ok)
}
})
}
}

View file

@ -1,38 +0,0 @@
package db
import (
"crypto/x509"
"github.com/pkg/errors"
)
// ErrNotImplemented is an error returned when an operation is Not Implemented.
var ErrNotImplemented = errors.Errorf("not implemented")
// NoopDB implements the DB interface with Noops
type NoopDB int
// Init noop
func (n *NoopDB) Init(c *Config) (AuthDB, error) {
return n, nil
}
// IsRevoked noop
func (n *NoopDB) IsRevoked(sn string) (bool, error) {
return false, nil
}
// Revoke returns a "NotImplemented" error.
func (n *NoopDB) Revoke(rci *RevokedCertificateInfo) error {
return ErrNotImplemented
}
// StoreCertificate returns a "NotImplemented" error.
func (n *NoopDB) StoreCertificate(crt *x509.Certificate) error {
return ErrNotImplemented
}
// Shutdown returns nil
func (n *NoopDB) Shutdown() error {
return nil
}

View file

@ -1,21 +0,0 @@
package db
import (
"testing"
"github.com/smallstep/assert"
)
func Test_noop(t *testing.T) {
db := new(NoopDB)
_db, err := db.Init(&Config{})
assert.FatalError(t, err)
assert.Equals(t, db, _db)
isRevoked, err := db.IsRevoked("foo")
assert.False(t, isRevoked)
assert.Nil(t, err)
assert.Equals(t, db.Revoke(&RevokedCertificateInfo{}), ErrNotImplemented)
}

63
db/simple.go Normal file
View file

@ -0,0 +1,63 @@
package db
import (
"crypto/x509"
"sync"
"time"
"github.com/pkg/errors"
)
// ErrNotImplemented is an error returned when an operation is Not Implemented.
var ErrNotImplemented = errors.Errorf("not implemented")
// SimpleDB is a barebones implementation of the DB interface. It is NOT an
// in memory implementation of the DB, but rather the bare minimum of
// functionality that the CA requires to operate securely.
type SimpleDB struct {
usedTokens *sync.Map
}
func newSimpleDB(c *Config) (AuthDB, error) {
db := &SimpleDB{}
db.usedTokens = new(sync.Map)
return db, nil
}
// IsRevoked noop
func (s *SimpleDB) IsRevoked(sn string) (bool, error) {
return false, nil
}
// Revoke returns a "NotImplemented" error.
func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error {
return ErrNotImplemented
}
// StoreCertificate returns a "NotImplemented" error.
func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error {
return ErrNotImplemented
}
type usedToken struct {
UsedAt int64 `json:"ua,omitempty"`
Token string `json:"tok,omitempty"`
}
// UseToken returns a "NotImplemented" error.
func (s *SimpleDB) UseToken(id, tok string) (bool, error) {
if _, ok := s.usedTokens.LoadOrStore(id, &usedToken{
UsedAt: time.Now().Unix(),
Token: tok,
}); ok {
// Token already exists in DB.
return false, nil
}
// Successfully stored token.
return true, nil
}
// Shutdown returns nil
func (s *SimpleDB) Shutdown() error {
return nil
}

37
db/simple_test.go Normal file
View file

@ -0,0 +1,37 @@
package db
import (
"testing"
"github.com/smallstep/assert"
)
func TestSimpleDB(t *testing.T) {
db, err := newSimpleDB(nil)
assert.FatalError(t, err)
// Revoke
assert.Equals(t, ErrNotImplemented, db.Revoke(nil))
// IsRevoked -- verify noop
isRevoked, err := db.IsRevoked("foo")
assert.False(t, isRevoked)
assert.Nil(t, err)
// StoreCertificate
assert.Equals(t, ErrNotImplemented, db.StoreCertificate(nil))
// UseToken
ok, err := db.UseToken("foo", "bar")
assert.True(t, ok)
assert.Nil(t, err)
ok, err = db.UseToken("foo", "cat")
assert.False(t, ok)
assert.Nil(t, err)
// Shutdown -- verify noop
assert.FatalError(t, db.Shutdown())
ok, err = db.UseToken("foo", "cat")
assert.False(t, ok)
assert.Nil(t, err)
}

View file

@ -1,4 +1,4 @@
FROM smallstep/step-cli:0.9.0 FROM smallstep/step-cli:latest
ARG BINPATH="bin/step-ca" ARG BINPATH="bin/step-ca"