Merge branch 'master' into step-sds
This commit is contained in:
commit
43c5831582
43 changed files with 2443 additions and 218 deletions
31
Gopkg.lock
generated
31
Gopkg.lock
generated
|
@ -228,12 +228,12 @@
|
|||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
|
||||
version = "v0.8.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6"
|
||||
|
@ -277,7 +277,7 @@
|
|||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:253eec7c89c6fe08ae020877b0b44343e86da68dac99207280e7ddcacd441f1f"
|
||||
digest = "1:0f02ebdfb2860d118922fa6abb2fe3b4d9984217b7f12761ac2d787f84fca67a"
|
||||
name = "github.com/smallstep/cli"
|
||||
packages = [
|
||||
"command",
|
||||
|
@ -298,7 +298,15 @@
|
|||
"utils",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f851b6b63d8d5e78b8a986057034d69fe904c477"
|
||||
revision = "25e732fe7e712d5b3009666fb968f9b9c188666b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fd8d9eb07509d8ef47fc82c99646f0b2203b2ba3c240ba77d8c457bb6109836d"
|
||||
name = "github.com/smallstep/nosql"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "d8f68d14f9ae04e0991dce06b44768f2d38dccf8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
@ -316,15 +324,24 @@
|
|||
pruneopts = "UT"
|
||||
revision = "b67dcf995b6a7b7f14fad5fcb7cc5441b05e814b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5f7414cf41466d4b4dd7ec52b2cd3e481e08cfd11e7e24fef730c0e483e88bb1"
|
||||
name = "go.etcd.io/bbolt"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "63597a96ec0ad9e6d43c3fc81e809909e0237461"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a068d4e48e0f2e172903d25b6e066815fa8efd4b01102aec4c741f02a9650c03"
|
||||
digest = "1:5dd7da6df07f42194cb25d162b4b89664ed7b08d7d4334f6a288393d54b095ce"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cryptobyte",
|
||||
"cryptobyte/asn1",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"ocsp",
|
||||
"pbkdf2",
|
||||
"ssh/terminal",
|
||||
]
|
||||
|
@ -574,8 +591,10 @@
|
|||
"github.com/smallstep/cli/token",
|
||||
"github.com/smallstep/cli/token/provision",
|
||||
"github.com/smallstep/cli/usage",
|
||||
"github.com/smallstep/nosql",
|
||||
"github.com/tsenart/deadcode",
|
||||
"github.com/urfave/cli",
|
||||
"golang.org/x/crypto/ocsp",
|
||||
"golang.org/x/net/context",
|
||||
"golang.org/x/net/http2",
|
||||
"google.golang.org/grpc",
|
||||
|
|
|
@ -48,6 +48,10 @@ required = [
|
|||
branch = "master"
|
||||
name = "github.com/smallstep/cli"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/smallstep/nosql"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
|
@ -25,12 +26,17 @@ import (
|
|||
|
||||
// Authority is the interface implemented by a CA authority.
|
||||
type Authority interface {
|
||||
// NOTE: Authorize will be deprecated in future releases. Please use the
|
||||
// context specific Authoirize[Sign|Revoke|etc.] methods.
|
||||
Authorize(ott string) ([]provisioner.SignOption, error)
|
||||
AuthorizeSign(ott string) ([]provisioner.SignOption, error)
|
||||
GetTLSOptions() *tlsutil.TLSOptions
|
||||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error)
|
||||
Renew(peer *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
|
||||
Revoke(*authority.RevokeOptions) error
|
||||
GetEncryptedKey(kid string) (string, error)
|
||||
GetRoots() (federation []*x509.Certificate, err error)
|
||||
GetFederation() ([]*x509.Certificate, error)
|
||||
|
@ -236,6 +242,7 @@ func (h *caHandler) Route(r Router) {
|
|||
r.MethodFunc("GET", "/root/{sha}", h.Root)
|
||||
r.MethodFunc("POST", "/sign", h.Sign)
|
||||
r.MethodFunc("POST", "/renew", h.Renew)
|
||||
r.MethodFunc("POST", "/revoke", h.Revoke)
|
||||
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
||||
r.MethodFunc("GET", "/roots", h.Roots)
|
||||
|
@ -285,7 +292,7 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
|
|||
NotAfter: body.NotAfter,
|
||||
}
|
||||
|
||||
signOpts, err := h.Authority.Authorize(body.OTT)
|
||||
signOpts, err := h.Authority.AuthorizeSign(body.OTT)
|
||||
if err != nil {
|
||||
WriteError(w, Unauthorized(err))
|
||||
return
|
||||
|
|
132
api/api_test.go
132
api/api_test.go
|
@ -24,6 +24,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
|
@ -407,23 +408,110 @@ func TestSignRequest_Validate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type mockAuthority struct {
|
||||
ret1, ret2 interface{}
|
||||
err error
|
||||
authorize func(ott string) ([]provisioner.SignOption, error)
|
||||
getTLSOptions func() *tlsutil.TLSOptions
|
||||
root func(shasum string) (*x509.Certificate, error)
|
||||
sign func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
getEncryptedKey func(kid string) (string, error)
|
||||
getRoots func() ([]*x509.Certificate, error)
|
||||
getFederation func() ([]*x509.Certificate, error)
|
||||
type mockProvisioner struct {
|
||||
ret1, ret2, ret3 interface{}
|
||||
err error
|
||||
getID func() string
|
||||
getTokenID func(string) (string, error)
|
||||
getName func() string
|
||||
getType func() provisioner.Type
|
||||
getEncryptedKey func() (string, string, bool)
|
||||
init func(provisioner.Config) error
|
||||
authorizeRevoke func(ott string) error
|
||||
authorizeSign func(ott string) ([]provisioner.SignOption, error)
|
||||
authorizeRenewal func(*x509.Certificate) error
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) GetID() string {
|
||||
if m.getID != nil {
|
||||
return m.getID()
|
||||
}
|
||||
return m.ret1.(string)
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) GetTokenID(token string) (string, error) {
|
||||
if m.getTokenID != nil {
|
||||
return m.getTokenID(token)
|
||||
}
|
||||
if m.ret1 == nil {
|
||||
return "", m.err
|
||||
}
|
||||
return m.ret1.(string), m.err
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) GetName() string {
|
||||
if m.getName != nil {
|
||||
return m.getName()
|
||||
}
|
||||
return m.ret1.(string)
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) GetType() provisioner.Type {
|
||||
if m.getType != nil {
|
||||
return m.getType()
|
||||
}
|
||||
return m.ret1.(provisioner.Type)
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) GetEncryptedKey() (string, string, bool) {
|
||||
if m.getEncryptedKey != nil {
|
||||
return m.getEncryptedKey()
|
||||
}
|
||||
return m.ret1.(string), m.ret2.(string), m.ret3.(bool)
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) Init(c provisioner.Config) error {
|
||||
if m.init != nil {
|
||||
return m.init(c)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) AuthorizeRevoke(ott string) error {
|
||||
if m.authorizeRevoke != nil {
|
||||
return m.authorizeRevoke(ott)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorizeSign != nil {
|
||||
return m.authorizeSign(ott)
|
||||
}
|
||||
return m.ret1.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
||||
func (m *mockProvisioner) AuthorizeRenewal(c *x509.Certificate) error {
|
||||
if m.authorizeRenewal != nil {
|
||||
return m.authorizeRenewal(c)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
type mockAuthority struct {
|
||||
ret1, ret2 interface{}
|
||||
err error
|
||||
authorizeSign func(ott string) ([]provisioner.SignOption, error)
|
||||
getTLSOptions func() *tlsutil.TLSOptions
|
||||
root func(shasum string) (*x509.Certificate, error)
|
||||
sign func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
revoke func(*authority.RevokeOptions) error
|
||||
getEncryptedKey func(kid string) (string, error)
|
||||
getRoots func() ([]*x509.Certificate, error)
|
||||
getFederation func() ([]*x509.Certificate, error)
|
||||
}
|
||||
|
||||
// TODO: remove once Authorize is deprecated.
|
||||
func (m *mockAuthority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorize != nil {
|
||||
return m.authorize(ott)
|
||||
return m.AuthorizeSign(ott)
|
||||
}
|
||||
|
||||
func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorizeSign != nil {
|
||||
return m.authorizeSign(ott)
|
||||
}
|
||||
return m.ret1.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
@ -463,6 +551,20 @@ func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provision
|
|||
return m.ret1.(provisioner.List), m.ret2.(string), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) {
|
||||
if m.loadProvisionerByCertificate != nil {
|
||||
return m.loadProvisionerByCertificate(cert)
|
||||
}
|
||||
return m.ret1.(provisioner.Interface), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Revoke(opts *authority.RevokeOptions) error {
|
||||
if m.revoke != nil {
|
||||
return m.revoke(opts)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {
|
||||
if m.getEncryptedKey != nil {
|
||||
return m.getEncryptedKey(kid)
|
||||
|
@ -617,7 +719,7 @@ func Test_caHandler_Sign(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := New(&mockAuthority{
|
||||
ret1: tt.cert, ret2: tt.root, err: tt.signErr,
|
||||
authorize: func(ott string) ([]provisioner.SignOption, error) {
|
||||
authorizeSign: func(ott string) ([]provisioner.SignOption, error) {
|
||||
return tt.certAttrOpts, tt.autherr
|
||||
},
|
||||
getTLSOptions: func() *tlsutil.TLSOptions {
|
||||
|
|
|
@ -82,6 +82,11 @@ func InternalServerError(err error) error {
|
|||
return NewError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
// NotImplemented returns a 500 error with the given error.
|
||||
func NotImplemented(err error) error {
|
||||
return NewError(http.StatusNotImplemented, err)
|
||||
}
|
||||
|
||||
// BadRequest returns an 400 error with the given error.
|
||||
func BadRequest(err error) error {
|
||||
return NewError(http.StatusBadRequest, err)
|
||||
|
|
105
api/revoke.go
Normal file
105
api/revoke.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// RevokeResponse is the response object that returns the health of the server.
|
||||
type RevokeResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// RevokeRequest is the request body for a revocation request.
|
||||
type RevokeRequest struct {
|
||||
Serial string `json:"serial"`
|
||||
OTT string `json:"ott"`
|
||||
ReasonCode int `json:"reasonCode"`
|
||||
Reason string `json:"reason"`
|
||||
Passive bool `json:"passive"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the RevokeRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (r *RevokeRequest) Validate() (err error) {
|
||||
if r.Serial == "" {
|
||||
return BadRequest(errors.New("missing serial"))
|
||||
}
|
||||
if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {
|
||||
return BadRequest(errors.New("reasonCode out of bounds"))
|
||||
}
|
||||
if !r.Passive {
|
||||
return NotImplemented(errors.New("non-passive revocation not implemented"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Revoke supports handful of different methods that revoke a Certificate.
|
||||
//
|
||||
// NOTE: currently only Passive revocation is supported.
|
||||
//
|
||||
// TODO: Add CRL and OCSP support.
|
||||
func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) {
|
||||
var body RevokeRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
|
||||
return
|
||||
}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &authority.RevokeOptions{
|
||||
Serial: body.Serial,
|
||||
Reason: body.Reason,
|
||||
ReasonCode: body.ReasonCode,
|
||||
PassiveOnly: body.Passive,
|
||||
}
|
||||
|
||||
// A token indicates that we are using the api via a provisioner token,
|
||||
// otherwise it is assumed that the certificate is revoking itself over mTLS.
|
||||
if len(body.OTT) > 0 {
|
||||
logOtt(w, body.OTT)
|
||||
opts.OTT = body.OTT
|
||||
} else {
|
||||
// If no token is present, then the request must be made over mTLS and
|
||||
// the client certificate Serial Number must match the serial number
|
||||
// being revoked.
|
||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||
WriteError(w, BadRequest(errors.New("missing ott or peer certificate")))
|
||||
return
|
||||
}
|
||||
opts.Crt = r.TLS.PeerCertificates[0]
|
||||
logCertificate(w, opts.Crt)
|
||||
opts.MTLS = true
|
||||
}
|
||||
|
||||
if err := h.Authority.Revoke(opts); err != nil {
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
}
|
||||
|
||||
logRevoke(w, opts)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
JSON(w, &RevokeResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
func logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"serial": ri.Serial,
|
||||
"reasonCode": ri.ReasonCode,
|
||||
"reason": ri.Reason,
|
||||
"passiveOnly": ri.PassiveOnly,
|
||||
"mTLS": ri.MTLS,
|
||||
})
|
||||
}
|
||||
}
|
234
api/revoke_test.go
Normal file
234
api/revoke_test.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
func TestRevokeRequestValidate(t *testing.T) {
|
||||
type test struct {
|
||||
rr *RevokeRequest
|
||||
err *Error
|
||||
}
|
||||
tests := map[string]test{
|
||||
"error/missing serial": {
|
||||
rr: &RevokeRequest{},
|
||||
err: &Error{Err: errors.New("missing serial"), Status: http.StatusBadRequest},
|
||||
},
|
||||
"error/bad reasonCode": {
|
||||
rr: &RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 15,
|
||||
Passive: true,
|
||||
},
|
||||
err: &Error{Err: errors.New("reasonCode out of bounds"), Status: http.StatusBadRequest},
|
||||
},
|
||||
"error/non-passive not implemented": {
|
||||
rr: &RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 8,
|
||||
Passive: false,
|
||||
},
|
||||
err: &Error{Err: errors.New("non-passive revocation not implemented"), Status: http.StatusNotImplemented},
|
||||
},
|
||||
"ok": {
|
||||
rr: &RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 9,
|
||||
Passive: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.rr.Validate(); err != nil {
|
||||
switch v := err.(type) {
|
||||
case *Error:
|
||||
assert.HasPrefix(t, v.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.StatusCode(), tc.err.Status)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_caHandler_Revoke(t *testing.T) {
|
||||
type test struct {
|
||||
input string
|
||||
auth Authority
|
||||
tls *tls.ConnectionState
|
||||
err error
|
||||
statusCode int
|
||||
expected []byte
|
||||
}
|
||||
tests := map[string]func(*testing.T) test{
|
||||
"400/json read error": func(t *testing.T) test {
|
||||
return test{
|
||||
input: "{",
|
||||
statusCode: http.StatusBadRequest,
|
||||
}
|
||||
},
|
||||
"400/invalid request body": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusBadRequest,
|
||||
}
|
||||
},
|
||||
"200/ott": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
OTT: "valid",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusOK,
|
||||
auth: &mockAuthority{
|
||||
revoke: func(opts *authority.RevokeOptions) error {
|
||||
assert.True(t, opts.PassiveOnly)
|
||||
assert.False(t, opts.MTLS)
|
||||
assert.Equals(t, opts.Serial, "sn")
|
||||
assert.Equals(t, opts.ReasonCode, 4)
|
||||
assert.Equals(t, opts.Reason, "foo")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expected: []byte(`{"status":"ok"}`),
|
||||
}
|
||||
},
|
||||
"400/no OTT and no peer certificate": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 4,
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusBadRequest,
|
||||
}
|
||||
},
|
||||
"200/no ott": func(t *testing.T) test {
|
||||
cs := &tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},
|
||||
}
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "1404354960355712309",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusOK,
|
||||
tls: cs,
|
||||
auth: &mockAuthority{
|
||||
revoke: func(ri *authority.RevokeOptions) error {
|
||||
assert.True(t, ri.PassiveOnly)
|
||||
assert.True(t, ri.MTLS)
|
||||
assert.Equals(t, ri.Serial, "1404354960355712309")
|
||||
assert.Equals(t, ri.ReasonCode, 4)
|
||||
assert.Equals(t, ri.Reason, "foo")
|
||||
return nil
|
||||
},
|
||||
loadProvisionerByCertificate: func(crt *x509.Certificate) (provisioner.Interface, error) {
|
||||
return &mockProvisioner{
|
||||
getID: func() string {
|
||||
return "mock-provisioner-id"
|
||||
},
|
||||
}, err
|
||||
},
|
||||
},
|
||||
expected: []byte(`{"status":"ok"}`),
|
||||
}
|
||||
},
|
||||
"500/ott authority.Revoke": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
OTT: "valid",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
auth: &mockAuthority{
|
||||
revoke: func(opts *authority.RevokeOptions) error {
|
||||
return InternalServerError(errors.New("force"))
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"403/ott authority.Revoke": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "sn",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
OTT: "valid",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusForbidden,
|
||||
auth: &mockAuthority{
|
||||
revoke: func(opts *authority.RevokeOptions) error {
|
||||
return errors.New("force")
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, _tc := range tests {
|
||||
tc := _tc(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := New(tc.auth).(*caHandler)
|
||||
req := httptest.NewRequest("POST", "http://example.com/revoke", strings.NewReader(tc.input))
|
||||
if tc.tls != nil {
|
||||
req.TLS = tc.tls
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
h.Revoke(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if tc.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tc.expected) {
|
||||
t.Errorf("caHandler.Root Body = %s, wants %s", body, tc.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
)
|
||||
|
@ -24,6 +25,7 @@ type Authority struct {
|
|||
ottMap *sync.Map
|
||||
startTime time.Time
|
||||
provisioners *provisioner.Collection
|
||||
db db.AuthDB
|
||||
// Do not re-initialize
|
||||
initOnce bool
|
||||
}
|
||||
|
@ -56,6 +58,12 @@ func (a *Authority) init() error {
|
|||
|
||||
var err error
|
||||
|
||||
// Initialize step-ca Database if defined in configuration.
|
||||
// If a.config.DB is nil then a noopDB will be returned.
|
||||
if a.db, err = db.New(a.config.DB); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load the root certificates and add them to the certificate store
|
||||
a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root))
|
||||
for i, path := range a.config.Root {
|
||||
|
@ -111,3 +119,8 @@ func (a *Authority) init() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
||||
func (a *Authority) Shutdown() error {
|
||||
return a.db.Shutdown()
|
||||
}
|
||||
|
|
|
@ -36,11 +36,19 @@ func testAuthority(t *testing.T) *Authority {
|
|||
DisableRenewal: &disableRenewal,
|
||||
},
|
||||
},
|
||||
&provisioner.JWK{
|
||||
Name: "renew_disabled",
|
||||
Type: "JWK",
|
||||
Key: maxjwk,
|
||||
Claims: &provisioner.Claims{
|
||||
DisableRenewal: &disableRenewal,
|
||||
},
|
||||
},
|
||||
}
|
||||
c := &Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{"testdata/secrets/root_ca.crt"},
|
||||
IntermediateCert: "testdata/secrets/intermediate_ca.crt",
|
||||
Root: []string{"testdata/certs/root_ca.crt"},
|
||||
IntermediateCert: "testdata/certs/intermediate_ca.crt",
|
||||
IntermediateKey: "testdata/secrets/intermediate_ca_key",
|
||||
DNSNames: []string{"test.ca.smallstep.com"},
|
||||
Password: "pass",
|
||||
|
|
|
@ -24,15 +24,16 @@ type Claims struct {
|
|||
Nonce string `json:"nonce,omitempty"`
|
||||
}
|
||||
|
||||
// Authorize authorizes a signature request by validating and authenticating
|
||||
// a OTT that must be sent w/ the request.
|
||||
func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
||||
// authorizeToken parses the token and returns the provisioner used to generate
|
||||
// the token. This method enforces the One-Time use policy (tokens can only be
|
||||
// used once).
|
||||
func (a *Authority) authorizeToken(ott string) (provisioner.Interface, error) {
|
||||
var errContext = map[string]interface{}{"ott": ott}
|
||||
|
||||
// Validate payload
|
||||
token, err := jose.ParseSigned(ott)
|
||||
if err != nil {
|
||||
return nil, &apiError{errors.Wrapf(err, "authorize: error parsing token"),
|
||||
return nil, &apiError{errors.Wrapf(err, "authorizeToken: error parsing token"),
|
||||
http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
|
@ -41,14 +42,15 @@ func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
|||
// before we can look up the provisioner.
|
||||
var claims Claims
|
||||
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||
return nil, &apiError{err, http.StatusUnauthorized, errContext}
|
||||
return nil, &apiError{errors.Wrap(err, "authorizeToken"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
// TODO: use new persistence layer abstraction.
|
||||
// Do not accept tokens issued before the start of the ca.
|
||||
// This check is meant as a stopgap solution to the current lack of a persistence layer.
|
||||
if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck {
|
||||
if claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) {
|
||||
return nil, &apiError{errors.New("authorize: token issued before the bootstrap of certificate authority"),
|
||||
return nil, &apiError{errors.New("authorizeToken: token issued before the bootstrap of certificate authority"),
|
||||
http.StatusUnauthorized, errContext}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +59,7 @@ func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
|||
p, ok := a.provisioners.LoadByToken(token, &claims.Claims)
|
||||
if !ok {
|
||||
return nil, &apiError{
|
||||
errors.Errorf("authorize: provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")),
|
||||
errors.Errorf("authorizeToken: provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")),
|
||||
http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
|
@ -74,19 +76,69 @@ func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
|||
UsedAt: time.Now().Unix(),
|
||||
Subject: claims.Subject,
|
||||
}); ok {
|
||||
return nil, &apiError{errors.Errorf("authorize: token already used"), http.StatusUnauthorized, errContext}
|
||||
return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the provisioner Authorize method to get the signing options
|
||||
opts, err := p.Authorize(ott)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Authorize is a passthrough to AuthorizeSign.
|
||||
// NOTE: Authorize will be deprecated in a future release. Please use the
|
||||
// context specific Authorize[Sign|Revoke|etc.] going forwards.
|
||||
func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
||||
return a.AuthorizeSign(ott)
|
||||
}
|
||||
|
||||
// AuthorizeSign authorizes a signature request by validating and authenticating
|
||||
// a OTT that must be sent w/ the request.
|
||||
func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
|
||||
var errContext = context{"ott": ott}
|
||||
|
||||
p, err := a.authorizeToken(ott)
|
||||
if err != nil {
|
||||
return nil, &apiError{errors.Wrap(err, "authorize"), http.StatusUnauthorized, errContext}
|
||||
return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
// Call the provisioner AuthorizeSign method to apply provisioner specific
|
||||
// auth claims and get the signing options.
|
||||
opts, err := p.AuthorizeSign(ott)
|
||||
if err != nil {
|
||||
return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// authorizeRevoke authorizes a revocation request by validating and authenticating
|
||||
// the RevokeOptions POSTed with the request.
|
||||
// Returns a tuple of the provisioner ID and error, if one occurred.
|
||||
func (a *Authority) authorizeRevoke(opts *RevokeOptions) (p provisioner.Interface, err error) {
|
||||
if opts.MTLS {
|
||||
if opts.Crt.SerialNumber.String() != opts.Serial {
|
||||
return nil, errors.New("authorizeRevoke: serial number in certificate different than body")
|
||||
}
|
||||
// Load the Certificate provisioner if one exists.
|
||||
p, err = a.LoadProvisionerByCertificate(opts.Crt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authorizeRevoke")
|
||||
}
|
||||
} else {
|
||||
// Gets the token provisioner and validates common token fields.
|
||||
p, err = a.authorizeToken(opts.OTT)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authorizeRevoke")
|
||||
}
|
||||
|
||||
// Call the provisioner AuthorizeRevoke to apply provisioner specific auth claims.
|
||||
err = p.AuthorizeRevoke(opts.OTT)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authorizeRevoke")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// authorizeRenewal tries to locate the step provisioner extension, and checks
|
||||
// if for the configured provisioner, the renewal is enabled or not. If the
|
||||
// extra extension cannot be found, authorize the renewal by default.
|
||||
|
@ -94,17 +146,35 @@ func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
|||
// TODO(mariano): should we authorize by default?
|
||||
func (a *Authority) authorizeRenewal(crt *x509.Certificate) error {
|
||||
errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()}
|
||||
|
||||
// Check the passive revocation table.
|
||||
isRevoked, err := a.db.IsRevoked(crt.SerialNumber.String())
|
||||
if err != nil {
|
||||
return &apiError{
|
||||
err: errors.Wrap(err, "renew"),
|
||||
code: http.StatusInternalServerError,
|
||||
context: errContext,
|
||||
}
|
||||
}
|
||||
if isRevoked {
|
||||
return &apiError{
|
||||
err: errors.New("renew: certificate has been revoked"),
|
||||
code: http.StatusUnauthorized,
|
||||
context: errContext,
|
||||
}
|
||||
}
|
||||
|
||||
p, ok := a.provisioners.LoadByCertificate(crt)
|
||||
if !ok {
|
||||
return &apiError{
|
||||
err: errors.New("provisioner not found"),
|
||||
err: errors.New("renew: provisioner not found"),
|
||||
code: http.StatusUnauthorized,
|
||||
context: errContext,
|
||||
}
|
||||
}
|
||||
if err := p.AuthorizeRenewal(crt); err != nil {
|
||||
return &apiError{
|
||||
err: err,
|
||||
err: errors.Wrap(err, "renew"),
|
||||
code: http.StatusUnauthorized,
|
||||
context: errContext,
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/randutil"
|
||||
"github.com/smallstep/cli/jose"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {
|
||||
|
@ -43,18 +46,20 @@ func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose
|
|||
return jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
func TestAuthority_authorizeToken(t *testing.T) {
|
||||
a := testAuthority(t)
|
||||
|
||||
key, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||
jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||
assert.FatalError(t, err)
|
||||
// Invalid keys
|
||||
keyNoKid := &jose.JSONWebKey{Key: key.Key, KeyID: ""}
|
||||
keyBadKid := &jose.JSONWebKey{Key: key.Key, KeyID: "foo"}
|
||||
|
||||
now := time.Now()
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
validIssuer := "step-cli"
|
||||
validAudience := []string{"https://test.ca.smallstep.com/sign"}
|
||||
validAudience := []string{"https://test.ca.smallstep.com/revoke"}
|
||||
|
||||
type authorizeTest struct {
|
||||
auth *Authority
|
||||
|
@ -63,83 +68,91 @@ func TestAuthorize(t *testing.T) {
|
|||
res []interface{}
|
||||
}
|
||||
tests := map[string]func(t *testing.T) *authorizeTest{
|
||||
"fail invalid ott": func(t *testing.T) *authorizeTest {
|
||||
"fail/invalid-ott": func(t *testing.T) *authorizeTest {
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: "foo",
|
||||
err: &apiError{errors.New("authorize: error parsing token"),
|
||||
err: &apiError{errors.New("authorizeToken: error parsing token"),
|
||||
http.StatusUnauthorized, context{"ott": "foo"}},
|
||||
}
|
||||
},
|
||||
"fail empty key id": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("test.smallstep.com", validIssuer, validAudience[0], nil, now, keyNoKid)
|
||||
"fail/prehistoric-token": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "test.smallstep.com",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(now.Add(-time.Hour)),
|
||||
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("authorize: provisioner not found or invalid audience"),
|
||||
err: &apiError{errors.New("authorizeToken: token issued before the bootstrap of certificate authority"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"fail provisioner not found": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("test.smallstep.com", validIssuer, validAudience[0], nil, now, keyBadKid)
|
||||
"fail/provisioner-not-found": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "test.smallstep.com",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
_sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo"))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw,
|
||||
err: &apiError{errors.New("authorize: provisioner not found or invalid audience"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"fail invalid issuer": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("test.smallstep.com", "invalid-issuer", validAudience[0], nil, now, key)
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw,
|
||||
err: &apiError{errors.New("authorize: provisioner not found or invalid audience"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"fail empty subject": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("", validIssuer, validAudience[0], nil, now, key)
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw,
|
||||
err: &apiError{errors.New("authorize: token subject cannot be empty"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"fail verify-sig-failure": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("test.smallstep.com", validIssuer, validAudience[0], nil, now, key)
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw + "00",
|
||||
err: &apiError{errors.New("authorize: error parsing claims: square/go-jose: error in cryptographic primitive"),
|
||||
http.StatusUnauthorized, context{"ott": raw + "00"}},
|
||||
}
|
||||
},
|
||||
"fail token-already-used": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("test.smallstep.com", validIssuer, validAudience[0], nil, now, key)
|
||||
assert.FatalError(t, err)
|
||||
_, err = a.Authorize(raw)
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw,
|
||||
err: &apiError{errors.New("authorize: token already used"),
|
||||
err: &apiError{errors.New("authorizeToken: provisioner not found or invalid audience (https://test.ca.smallstep.com/revoke)"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) *authorizeTest {
|
||||
raw, err := generateToken("test.smallstep.com", validIssuer, validAudience[0], nil, now, key)
|
||||
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,
|
||||
res: []interface{}{"1", "2", "3", "4", "5", "6"},
|
||||
}
|
||||
},
|
||||
"fail/token-already-used": func(t *testing.T) *authorizeTest {
|
||||
_a := testAuthority(t)
|
||||
|
||||
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)
|
||||
_, err = _a.authorizeToken(raw)
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: _a,
|
||||
ott: raw,
|
||||
err: &apiError{errors.New("authorizeToken: token already used"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -147,9 +160,8 @@ func TestAuthorize(t *testing.T) {
|
|||
for name, genTestCase := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
crtOpts, err := tc.auth.Authorize(tc.ott)
|
||||
p, err := tc.auth.authorizeToken(tc.ott)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch v := err.(type) {
|
||||
|
@ -163,9 +175,415 @@ func TestAuthorize(t *testing.T) {
|
|||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, len(crtOpts), len(tc.res))
|
||||
assert.Equals(t, p.GetID(), "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthority_authorizeRevoke(t *testing.T) {
|
||||
a := testAuthority(t)
|
||||
|
||||
jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
validIssuer := "step-cli"
|
||||
validAudience := []string{"https://test.ca.smallstep.com/revoke"}
|
||||
|
||||
type authorizeTest struct {
|
||||
auth *Authority
|
||||
opts *RevokeOptions
|
||||
err error
|
||||
res []interface{}
|
||||
}
|
||||
tests := map[string]func(t *testing.T) *authorizeTest{
|
||||
"fail/token/invalid-ott": func(t *testing.T) *authorizeTest {
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
opts: &RevokeOptions{OTT: "foo"},
|
||||
err: errors.New("authorizeRevoke: authorizeToken: error parsing token"),
|
||||
}
|
||||
},
|
||||
"fail/token/invalid-subject": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "",
|
||||
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,
|
||||
opts: &RevokeOptions{OTT: raw},
|
||||
err: errors.New("authorizeRevoke: token subject cannot be empty"),
|
||||
}
|
||||
},
|
||||
"ok/token": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "test.smallstep.com",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
opts: &RevokeOptions{OTT: raw},
|
||||
}
|
||||
},
|
||||
"fail/mTLS/invalid-serial": func(t *testing.T) *authorizeTest {
|
||||
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
opts: &RevokeOptions{MTLS: true, Crt: crt, Serial: "foo"},
|
||||
err: errors.New("authorizeRevoke: serial number in certificate different than body"),
|
||||
}
|
||||
},
|
||||
"fail/mTLS/load-provisioner": func(t *testing.T) *authorizeTest {
|
||||
crt, err := pemutil.ReadCertificate("./testdata/certs/provisioner-not-found.crt")
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
opts: &RevokeOptions{MTLS: true, Crt: crt, Serial: "41633491264736369593451462439668497527"},
|
||||
err: errors.New("authorizeRevoke: provisioner not found"),
|
||||
}
|
||||
},
|
||||
"ok/mTLS": func(t *testing.T) *authorizeTest {
|
||||
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
opts: &RevokeOptions{MTLS: true, Crt: crt, Serial: "102012593071130646873265215610956555026"},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, genTestCase := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
|
||||
p, err := tc.auth.authorizeRevoke(tc.opts)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
if assert.NotNil(t, p) {
|
||||
assert.Equals(t, p.GetID(), "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthority_AuthorizeSign(t *testing.T) {
|
||||
a := testAuthority(t)
|
||||
|
||||
jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
validIssuer := "step-cli"
|
||||
validAudience := []string{"https://test.ca.smallstep.com/sign"}
|
||||
|
||||
type authorizeTest struct {
|
||||
auth *Authority
|
||||
ott string
|
||||
err *apiError
|
||||
res []interface{}
|
||||
}
|
||||
tests := map[string]func(t *testing.T) *authorizeTest{
|
||||
"fail/invalid-ott": func(t *testing.T) *authorizeTest {
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: "foo",
|
||||
err: &apiError{errors.New("authorizeSign: authorizeToken: error parsing token"),
|
||||
http.StatusUnauthorized, context{"ott": "foo"}},
|
||||
}
|
||||
},
|
||||
"fail/invalid-subject": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "",
|
||||
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("authorizeSign: token subject cannot be empty"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "test.smallstep.com",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, genTestCase := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
|
||||
got, err := tc.auth.AuthorizeSign(tc.ott)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Nil(t, got)
|
||||
switch v := err.(type) {
|
||||
case *apiError:
|
||||
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.code, tc.err.code)
|
||||
assert.Equals(t, v.context, tc.err.context)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Len(t, 6, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove once Authorize deprecated.
|
||||
func TestAuthority_Authorize(t *testing.T) {
|
||||
a := testAuthority(t)
|
||||
|
||||
jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
validIssuer := "step-cli"
|
||||
validAudience := []string{"https://test.ca.smallstep.com/sign"}
|
||||
|
||||
type authorizeTest struct {
|
||||
auth *Authority
|
||||
ott string
|
||||
err *apiError
|
||||
res []interface{}
|
||||
}
|
||||
tests := map[string]func(t *testing.T) *authorizeTest{
|
||||
"fail/invalid-ott": func(t *testing.T) *authorizeTest {
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: "foo",
|
||||
err: &apiError{errors.New("authorizeSign: authorizeToken: error parsing token"),
|
||||
http.StatusUnauthorized, context{"ott": "foo"}},
|
||||
}
|
||||
},
|
||||
"fail/invalid-subject": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "",
|
||||
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("authorizeSign: token subject cannot be empty"),
|
||||
http.StatusUnauthorized, context{"ott": raw}},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) *authorizeTest {
|
||||
cl := jwt.Claims{
|
||||
Subject: "test.smallstep.com",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
ott: raw,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, genTestCase := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
|
||||
got, err := tc.auth.Authorize(tc.ott)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Nil(t, got)
|
||||
switch v := err.(type) {
|
||||
case *apiError:
|
||||
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.code, tc.err.code)
|
||||
assert.Equals(t, v.context, tc.err.context)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Len(t, 6, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthority_authorizeRenewal(t *testing.T) {
|
||||
fooCrt, err := pemutil.ReadCertificate("testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
renewDisabledCrt, err := pemutil.ReadCertificate("testdata/certs/renew-disabled.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
otherCrt, err := pemutil.ReadCertificate("testdata/certs/provisioner-not-found.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type authorizeTest struct {
|
||||
auth *Authority
|
||||
crt *x509.Certificate
|
||||
err *apiError
|
||||
}
|
||||
tests := map[string]func(t *testing.T) *authorizeTest{
|
||||
"fail/db.IsRevoked-error": func(t *testing.T) *authorizeTest {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{
|
||||
isRevoked: func(key string) (bool, error) {
|
||||
return false, errors.New("force")
|
||||
},
|
||||
}
|
||||
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
crt: fooCrt,
|
||||
err: &apiError{errors.New("renew: force"),
|
||||
http.StatusInternalServerError, context{"serialNumber": "102012593071130646873265215610956555026"}},
|
||||
}
|
||||
},
|
||||
"fail/revoked": func(t *testing.T) *authorizeTest {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{
|
||||
isRevoked: func(key string) (bool, error) {
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
crt: fooCrt,
|
||||
err: &apiError{errors.New("renew: certificate has been revoked"),
|
||||
http.StatusUnauthorized, context{"serialNumber": "102012593071130646873265215610956555026"}},
|
||||
}
|
||||
},
|
||||
"fail/load-provisioner": func(t *testing.T) *authorizeTest {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{
|
||||
isRevoked: func(key string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
crt: otherCrt,
|
||||
err: &apiError{errors.New("renew: provisioner not found"),
|
||||
http.StatusUnauthorized, context{"serialNumber": "41633491264736369593451462439668497527"}},
|
||||
}
|
||||
},
|
||||
"fail/provisioner-authorize-renewal-fail": func(t *testing.T) *authorizeTest {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{
|
||||
isRevoked: func(key string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
crt: renewDisabledCrt,
|
||||
err: &apiError{errors.New("renew: renew is disabled for provisioner renew_disabled:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
|
||||
http.StatusUnauthorized, context{"serialNumber": "119772236532068856521070735128919532568"}},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) *authorizeTest {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{
|
||||
isRevoked: func(key string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
return &authorizeTest{
|
||||
auth: a,
|
||||
crt: fooCrt,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, genTestCase := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
|
||||
err := tc.auth.authorizeRenewal(tc.crt)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch v := err.(type) {
|
||||
case *apiError:
|
||||
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.code, tc.err.code)
|
||||
assert.Equals(t, v.context, tc.err.context)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
)
|
||||
|
@ -28,9 +29,9 @@ var (
|
|||
}
|
||||
defaultDisableRenewal = false
|
||||
globalProvisionerClaims = provisioner.Claims{
|
||||
MinTLSDur: &provisioner.Duration{5 * time.Minute},
|
||||
MaxTLSDur: &provisioner.Duration{24 * time.Hour},
|
||||
DefaultTLSDur: &provisioner.Duration{24 * time.Hour},
|
||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
|
||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DisableRenewal: &defaultDisableRenewal,
|
||||
}
|
||||
)
|
||||
|
@ -44,6 +45,7 @@ type Config struct {
|
|||
Address string `json:"address"`
|
||||
DNSNames []string `json:"dnsNames"`
|
||||
Logger json.RawMessage `json:"logger,omitempty"`
|
||||
DB *db.Config `json:"db,omitempty"`
|
||||
Monitoring json.RawMessage `json:"monitoring,omitempty"`
|
||||
AuthorityConfig *AuthConfig `json:"authority,omitempty"`
|
||||
TLS *tlsutil.TLSOptions `json:"tls,omitempty"`
|
||||
|
@ -59,7 +61,7 @@ type AuthConfig struct {
|
|||
}
|
||||
|
||||
// Validate validates the authority configuration.
|
||||
func (c *AuthConfig) Validate(audiences []string) error {
|
||||
func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
|
||||
if c == nil {
|
||||
return errors.New("authority cannot be undefined")
|
||||
}
|
||||
|
@ -168,10 +170,18 @@ func (c *Config) Validate() error {
|
|||
// getAudiences returns the legacy and possible urls without the ports that will
|
||||
// be used as the default provisioner audiences. The CA might have proxies in
|
||||
// front so we cannot rely on the port.
|
||||
func (c *Config) getAudiences() []string {
|
||||
audiences := []string{legacyAuthority}
|
||||
for _, name := range c.DNSNames {
|
||||
audiences = append(audiences, fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name))
|
||||
func (c *Config) getAudiences() provisioner.Audiences {
|
||||
audiences := provisioner.Audiences{
|
||||
Sign: []string{legacyAuthority},
|
||||
Revoke: []string{legacyAuthority},
|
||||
}
|
||||
|
||||
for _, name := range c.DNSNames {
|
||||
audiences.Sign = append(audiences.Sign,
|
||||
fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name))
|
||||
audiences.Revoke = append(audiences.Revoke,
|
||||
fmt.Sprintf("https://%s/revoke", name), fmt.Sprintf("https://%s/1.0/revoke", name))
|
||||
}
|
||||
|
||||
return audiences
|
||||
}
|
||||
|
|
|
@ -277,7 +277,7 @@ func TestAuthConfigValidate(t *testing.T) {
|
|||
ac: &AuthConfig{
|
||||
Provisioners: p,
|
||||
Claims: &provisioner.Claims{
|
||||
MinTLSDur: &provisioner.Duration{-1},
|
||||
MinTLSDur: &provisioner.Duration{Duration: -1},
|
||||
},
|
||||
},
|
||||
err: errors.New("claims: MinTLSCertDuration must be greater than 0"),
|
||||
|
@ -305,7 +305,7 @@ func TestAuthConfigValidate(t *testing.T) {
|
|||
for name, get := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := get(t)
|
||||
err := tc.ac.Validate([]string{})
|
||||
err := tc.ac.Validate(provisioner.Audiences{})
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Equals(t, tc.err.Error(), err.Error())
|
||||
|
|
55
authority/db_test.go
Normal file
55
authority/db_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/smallstep/certificates/db"
|
||||
)
|
||||
|
||||
type MockAuthDB struct {
|
||||
err error
|
||||
ret1, ret2 interface{}
|
||||
init func(*db.Config) (db.AuthDB, error)
|
||||
isRevoked func(string) (bool, error)
|
||||
revoke func(rci *db.RevokedCertificateInfo) error
|
||||
storeCertificate func(crt *x509.Certificate) error
|
||||
shutdown func() error
|
||||
}
|
||||
|
||||
func (m *MockAuthDB) Init(c *db.Config) (db.AuthDB, error) {
|
||||
if m.init != nil {
|
||||
return m.init(c)
|
||||
}
|
||||
if m.ret1 == nil {
|
||||
return nil, m.err
|
||||
}
|
||||
return m.ret1.(*db.DB), m.err
|
||||
}
|
||||
|
||||
func (m *MockAuthDB) IsRevoked(sn string) (bool, error) {
|
||||
if m.isRevoked != nil {
|
||||
return m.isRevoked(sn)
|
||||
}
|
||||
return m.ret1.(bool), m.err
|
||||
}
|
||||
|
||||
func (m *MockAuthDB) Revoke(rci *db.RevokedCertificateInfo) error {
|
||||
if m.revoke != nil {
|
||||
return m.revoke(rci)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error {
|
||||
if m.storeCertificate != nil {
|
||||
return m.storeCertificate(crt)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockAuthDB) Shutdown() error {
|
||||
if m.shutdown != nil {
|
||||
return m.shutdown()
|
||||
}
|
||||
return m.err
|
||||
}
|
|
@ -38,12 +38,12 @@ type Collection struct {
|
|||
byID *sync.Map
|
||||
byKey *sync.Map
|
||||
sorted provisionerSlice
|
||||
audiences []string
|
||||
audiences Audiences
|
||||
}
|
||||
|
||||
// NewCollection initializes a collection of provisioners. The given list of
|
||||
// audiences are the audiences used by the JWT provisioner.
|
||||
func NewCollection(audiences []string) *Collection {
|
||||
func NewCollection(audiences Audiences) *Collection {
|
||||
return &Collection{
|
||||
byID: new(sync.Map),
|
||||
byKey: new(sync.Map),
|
||||
|
@ -59,7 +59,7 @@ func (c *Collection) Load(id string) (Interface, bool) {
|
|||
// LoadByToken parses the token claims and loads the provisioner associated.
|
||||
func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims) (Interface, bool) {
|
||||
// match with server audiences
|
||||
if matchesAudience(claims.Audience, c.audiences) {
|
||||
if matchesAudience(claims.Audience, c.audiences.All()) {
|
||||
// If matches with stored audiences it will be a JWT token (default), and
|
||||
// the id would be <issuer>:<kid>.
|
||||
return c.Load(claims.Issuer + ":" + token.Headers[0].KeyID)
|
||||
|
|
|
@ -68,14 +68,14 @@ func TestCollection_LoadByToken(t *testing.T) {
|
|||
|
||||
jwk, err := decryptJSONWebKey(p1.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
token, err := generateSimpleToken(p1.Name, testAudiences[0], jwk)
|
||||
token, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], jwk)
|
||||
assert.FatalError(t, err)
|
||||
t1, c1, err := parseToken(token)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
jwk, err = decryptJSONWebKey(p2.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
token, err = generateSimpleToken(p2.Name, testAudiences[1], jwk)
|
||||
token, err = generateSimpleToken(p2.Name, testAudiences.Sign[1], jwk)
|
||||
assert.FatalError(t, err)
|
||||
t2, c2, err := parseToken(token)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -92,7 +92,7 @@ func TestCollection_LoadByToken(t *testing.T) {
|
|||
|
||||
type fields struct {
|
||||
byID *sync.Map
|
||||
audiences []string
|
||||
audiences Audiences
|
||||
}
|
||||
type args struct {
|
||||
token *jose.JSONWebToken
|
||||
|
@ -109,7 +109,7 @@ func TestCollection_LoadByToken(t *testing.T) {
|
|||
{"ok2", fields{byID, testAudiences}, args{t2, c2}, p2, true},
|
||||
{"ok3", fields{byID, testAudiences}, args{t3, c3}, p3, true},
|
||||
{"bad", fields{byID, testAudiences}, args{t4, c4}, nil, false},
|
||||
{"fail", fields{byID, []string{"https://foo"}}, args{t1, c1}, nil, false},
|
||||
{"fail", fields{byID, Audiences{Sign: []string{"https://foo"}}}, args{t1, c1}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -162,7 +162,7 @@ func TestCollection_LoadByCertificate(t *testing.T) {
|
|||
|
||||
type fields struct {
|
||||
byID *sync.Map
|
||||
audiences []string
|
||||
audiences Audiences
|
||||
}
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
|
|
|
@ -24,7 +24,7 @@ type JWK struct {
|
|||
EncryptedKey string `json:"encryptedKey,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
claimer *Claimer
|
||||
audiences []string
|
||||
audiences Audiences
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name and credential id
|
||||
|
@ -33,6 +33,25 @@ func (p *JWK) GetID() string {
|
|||
return p.Name + ":" + p.Key.KeyID
|
||||
}
|
||||
|
||||
//
|
||||
// GetTokenID returns the identifier of the token.
|
||||
func (p *JWK) GetTokenID(ott string) (string, error) {
|
||||
// Validate payload
|
||||
token, err := jose.ParseSigned(ott)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
|
||||
// Get claims w/out verification. We need to look up the provisioner
|
||||
// key in order to verify the claims and we need the issuer from the claims
|
||||
// before we can look up the provisioner.
|
||||
var claims jose.Claims
|
||||
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||
return "", errors.Wrap(err, "error verifying claims")
|
||||
}
|
||||
return claims.ID, nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the provisioner.
|
||||
func (p *JWK) GetName() string {
|
||||
return p.Name
|
||||
|
@ -68,8 +87,10 @@ func (p *JWK) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Authorize validates the given token.
|
||||
func (p *JWK) Authorize(token string) ([]SignOption, error) {
|
||||
// authorizeToken performs common jwt authorization actions and returns the
|
||||
// claims for case specific downstream parsing.
|
||||
// e.g. a Sign request will auth/validate different fields than a Revoke request.
|
||||
func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, error) {
|
||||
jwt, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing token")
|
||||
|
@ -90,7 +111,7 @@ func (p *JWK) Authorize(token string) ([]SignOption, error) {
|
|||
}
|
||||
|
||||
// validate audiences with the defaults
|
||||
if !matchesAudience(claims.Audience, p.audiences) {
|
||||
if !matchesAudience(claims.Audience, audiences) {
|
||||
return nil, errors.New("invalid token: invalid audience claim (aud)")
|
||||
}
|
||||
|
||||
|
@ -98,6 +119,22 @@ func (p *JWK) Authorize(token string) ([]SignOption, error) {
|
|||
return nil, errors.New("token subject cannot be empty")
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error if the provisioner does not have rights to
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
func (p *JWK) AuthorizeRevoke(token string) error {
|
||||
_, err := p.authorizeToken(token, p.audiences.Revoke)
|
||||
return err
|
||||
}
|
||||
|
||||
// AuthorizeSign validates the given token.
|
||||
func (p *JWK) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
claims, err := p.authorizeToken(token, p.audiences.Sign)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// NOTE: This is for backwards compatibility with older versions of cli
|
||||
// and certificates. Older versions added the token subject as the only SAN
|
||||
// in a CSR by default.
|
||||
|
@ -123,9 +160,3 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error if the provisioner does not have rights to
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
func (p *JWK) AuthorizeRevoke(token string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ func TestJWK_Init(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestJWK_Authorize(t *testing.T) {
|
||||
func TestJWK_authorizeToken(t *testing.T) {
|
||||
p1, err := generateJWK()
|
||||
assert.FatalError(t, err)
|
||||
p2, err := generateJWK()
|
||||
|
@ -121,11 +121,11 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
key2, err := decryptJSONWebKey(p2.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
t1, err := generateSimpleToken(p1.Name, testAudiences[0], key1)
|
||||
t1, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key1)
|
||||
assert.FatalError(t, err)
|
||||
t2, err := generateSimpleToken(p2.Name, testAudiences[1], key2)
|
||||
t2, err := generateSimpleToken(p2.Name, testAudiences.Sign[1], key2)
|
||||
assert.FatalError(t, err)
|
||||
t3, err := generateToken("test.smallstep.com", p1.Name, testAudiences[0], "", []string{}, time.Now(), key1)
|
||||
t3, err := generateToken("test.smallstep.com", p1.Name, testAudiences.Sign[0], "", []string{}, time.Now(), key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Invalid tokens
|
||||
|
@ -133,14 +133,14 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
key3, err := generateJSONWebKey()
|
||||
assert.FatalError(t, err)
|
||||
// missing key
|
||||
failKey, err := generateSimpleToken(p1.Name, testAudiences[0], key3)
|
||||
failKey, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key3)
|
||||
assert.FatalError(t, err)
|
||||
// invalid token
|
||||
failTok := "foo." + parts[1] + "." + parts[2]
|
||||
// invalid claims
|
||||
failClaims := parts[0] + ".foo." + parts[1]
|
||||
// invalid issuer
|
||||
failIss, err := generateSimpleToken("foobar", testAudiences[0], key1)
|
||||
failIss, err := generateSimpleToken("foobar", testAudiences.Sign[0], key1)
|
||||
assert.FatalError(t, err)
|
||||
// invalid audience
|
||||
failAud, err := generateSimpleToken(p1.Name, "foobar", key1)
|
||||
|
@ -148,13 +148,13 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
// invalid signature
|
||||
failSig := t1[0 : len(t1)-2]
|
||||
// no subject
|
||||
failSub, err := generateToken("", p1.Name, testAudiences[0], "", []string{"test.smallstep.com"}, time.Now(), key1)
|
||||
failSub, err := generateToken("", p1.Name, testAudiences.Sign[0], "", []string{"test.smallstep.com"}, time.Now(), key1)
|
||||
assert.FatalError(t, err)
|
||||
// expired
|
||||
failExp, err := generateToken("subject", p1.Name, testAudiences[0], "", []string{"test.smallstep.com"}, time.Now().Add(-360*time.Second), key1)
|
||||
failExp, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "", []string{"test.smallstep.com"}, time.Now().Add(-360*time.Second), key1)
|
||||
assert.FatalError(t, err)
|
||||
// not before
|
||||
failNbf, err := generateToken("subject", p1.Name, testAudiences[0], "", []string{"test.smallstep.com"}, time.Now().Add(360*time.Second), key1)
|
||||
failNbf, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "", []string{"test.smallstep.com"}, time.Now().Add(360*time.Second), key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Remove encrypted key for p2
|
||||
|
@ -164,36 +164,123 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *JWK
|
||||
args args
|
||||
wantErr bool
|
||||
name string
|
||||
prov *JWK
|
||||
args args
|
||||
err error
|
||||
}{
|
||||
{"ok", p1, args{t1}, false},
|
||||
{"ok-no-encrypted-key", p2, args{t2}, false},
|
||||
{"ok-no-sans", p1, args{t3}, false},
|
||||
{"fail-key", p1, args{failKey}, true},
|
||||
{"fail-token", p1, args{failTok}, true},
|
||||
{"fail-claims", p1, args{failClaims}, true},
|
||||
{"fail-issuer", p1, args{failIss}, true},
|
||||
{"fail-audience", p1, args{failAud}, true},
|
||||
{"fail-signature", p1, args{failSig}, true},
|
||||
{"fail-subject", p1, args{failSub}, true},
|
||||
{"fail-expired", p1, args{failExp}, true},
|
||||
{"fail-not-before", p1, args{failNbf}, true},
|
||||
{"fail-token", p1, args{failTok}, errors.New("error parsing token")},
|
||||
{"fail-key", p1, args{failKey}, errors.New("error parsing claims")},
|
||||
{"fail-claims", p1, args{failClaims}, errors.New("error parsing claims")},
|
||||
{"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
|
||||
{"fail-issuer", p1, args{failIss}, errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)")},
|
||||
{"fail-expired", p1, args{failExp}, errors.New("invalid token: square/go-jose/jwt: validation failed, token is expired (exp)")},
|
||||
{"fail-not-before", p1, args{failNbf}, errors.New("invalid token: square/go-jose/jwt: validation failed, token not valid yet (nbf)")},
|
||||
{"fail-audience", p1, args{failAud}, errors.New("invalid token: invalid audience claim (aud)")},
|
||||
{"fail-subject", p1, args{failSub}, errors.New("token subject cannot be empty")},
|
||||
{"ok", p1, args{t1}, nil},
|
||||
{"ok-no-encrypted-key", p2, args{t2}, nil},
|
||||
{"ok-no-sans", p1, args{t3}, nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.prov.Authorize(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWK.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
assert.Nil(t, got)
|
||||
if got, err := tt.prov.authorizeToken(tt.args.token, testAudiences.Sign); err != nil {
|
||||
if assert.NotNil(t, tt.err) {
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tt.err)
|
||||
assert.NotNil(t, got)
|
||||
assert.Len(t, 6, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWK_AuthorizeRevoke(t *testing.T) {
|
||||
p1, err := generateJWK()
|
||||
assert.FatalError(t, err)
|
||||
key1, err := decryptJSONWebKey(p1.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
t1, err := generateSimpleToken(p1.Name, testAudiences.Revoke[0], key1)
|
||||
assert.FatalError(t, err)
|
||||
// invalid signature
|
||||
failSig := t1[0 : len(t1)-2]
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *JWK
|
||||
args args
|
||||
err error
|
||||
}{
|
||||
{"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
|
||||
{"ok", p1, args{t1}, nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.prov.AuthorizeRevoke(tt.args.token); err != nil {
|
||||
if assert.NotNil(t, tt.err) {
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWK_AuthorizeSign(t *testing.T) {
|
||||
p1, err := generateJWK()
|
||||
assert.FatalError(t, err)
|
||||
key1, err := decryptJSONWebKey(p1.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
t1, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// invalid signature
|
||||
failSig := t1[0 : len(t1)-2]
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *JWK
|
||||
args args
|
||||
err error
|
||||
}{
|
||||
{"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
|
||||
{"ok-sans", p1, args{t1}, nil},
|
||||
{"ok-no-sans", p1, args{t2}, nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, err := tt.prov.AuthorizeSign(tt.args.token); err != nil {
|
||||
if assert.NotNil(t, tt.err) {
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Len(t, 6, got)
|
||||
|
||||
_cnv := got[0]
|
||||
cnv, ok := _cnv.(commonNameValidator)
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, string(cnv), "subject")
|
||||
|
||||
_dnv := got[1]
|
||||
dnv, ok := _dnv.(dnsNamesValidator)
|
||||
assert.True(t, ok)
|
||||
if tt.name == "ok-sans" {
|
||||
assert.Equals(t, []string(dnv), []string{"test.smallstep.com"})
|
||||
} else {
|
||||
assert.Equals(t, []string(dnv), []string{"subject"})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -231,31 +318,3 @@ func TestJWK_AuthorizeRenewal(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWK_AuthorizeRevoke(t *testing.T) {
|
||||
p1, err := generateJWK()
|
||||
assert.FatalError(t, err)
|
||||
key1, err := decryptJSONWebKey(p1.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
t1, err := generateSimpleToken(p1.Name, testAudiences[0], key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *JWK
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"disabled", p1, args{t1}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.prov.AuthorizeRevoke(tt.args.token); (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWK.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ func (p *noop) GetID() string {
|
|||
return "noop"
|
||||
}
|
||||
|
||||
func (p *noop) GetTokenID(token string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *noop) GetName() string {
|
||||
return "noop"
|
||||
}
|
||||
|
@ -24,7 +28,7 @@ func (p *noop) Init(config Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *noop) Authorize(token string) ([]SignOption, error) {
|
||||
func (p *noop) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
return []SignOption{}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ func Test_noop(t *testing.T) {
|
|||
assert.Equals(t, "", key)
|
||||
assert.Equals(t, false, ok)
|
||||
|
||||
sigOptions, err := p.Authorize("foo")
|
||||
sigOptions, err := p.AuthorizeSign("foo")
|
||||
assert.Equals(t, []SignOption{}, sigOptions)
|
||||
assert.Equals(t, nil, err)
|
||||
}
|
||||
|
|
|
@ -83,6 +83,25 @@ func (o *OIDC) GetID() string {
|
|||
return o.ClientID
|
||||
}
|
||||
|
||||
// GetTokenID returns the provisioner unique identifier, the OIDC provisioner the
|
||||
// uses the clientID for this.
|
||||
func (o *OIDC) GetTokenID(ott string) (string, error) {
|
||||
// Validate payload
|
||||
token, err := jose.ParseSigned(ott)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
|
||||
// Get claims w/out verification. We need to look up the provisioner
|
||||
// key in order to verify the claims and we need the issuer from the claims
|
||||
// before we can look up the provisioner.
|
||||
var claims openIDPayload
|
||||
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||
return "", errors.Wrap(err, "error verifying claims")
|
||||
}
|
||||
return claims.Nonce, nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the provisioner.
|
||||
func (o *OIDC) GetName() string {
|
||||
return o.Name
|
||||
|
@ -171,8 +190,9 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Authorize validates the given token.
|
||||
func (o *OIDC) Authorize(token string) ([]SignOption, error) {
|
||||
// authorizeToken applies the most common provisioner authorization claims,
|
||||
// leaving the rest to context specific methods.
|
||||
func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) {
|
||||
jwt, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing token")
|
||||
|
@ -201,6 +221,31 @@ func (o *OIDC) Authorize(token string) ([]SignOption, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error if the provisioner does not have rights to
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
// Only tokens generated by an admin have the right to revoke a certificate.
|
||||
func (o *OIDC) AuthorizeRevoke(token string) error {
|
||||
claims, err := o.authorizeToken(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only admins can revoke certificates.
|
||||
if o.IsAdmin(claims.Email) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot revoke with non-admin token")
|
||||
}
|
||||
|
||||
// AuthorizeSign validates the given token.
|
||||
func (o *OIDC) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
claims, err := o.authorizeToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Admins should be able to authorize any SAN
|
||||
if o.IsAdmin(claims.Email) {
|
||||
return []SignOption{
|
||||
|
@ -226,12 +271,6 @@ func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error if the provisioner does not have rights to
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
func (o *OIDC) AuthorizeRevoke(token string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func getAndDecode(uri string, v interface{}) error {
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
|
|
|
@ -122,7 +122,7 @@ func TestOIDC_Init(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOIDC_Authorize(t *testing.T) {
|
||||
func TestOIDC_authorizeToken(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
|
||||
|
@ -153,12 +153,6 @@ func TestOIDC_Authorize(t *testing.T) {
|
|||
assert.FatalError(t, err)
|
||||
t2, err := generateSimpleToken("the-issuer", p2.ClientID, &keys.Keys[1])
|
||||
assert.FatalError(t, err)
|
||||
t3, err := generateSimpleToken("the-issuer", p3.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Admin email not in domains
|
||||
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// Invalid email
|
||||
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
@ -202,8 +196,6 @@ func TestOIDC_Authorize(t *testing.T) {
|
|||
}{
|
||||
{"ok1", p1, args{t1}, false},
|
||||
{"ok2", p2, args{t2}, false},
|
||||
{"admin", p3, args{t3}, false},
|
||||
{"admin", p3, args{okAdmin}, false},
|
||||
{"fail-email", p3, args{failEmail}, true},
|
||||
{"fail-domain", p3, args{failDomain}, true},
|
||||
{"fail-key", p1, args{failKey}, true},
|
||||
|
@ -217,7 +209,74 @@ func TestOIDC_Authorize(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.prov.Authorize(tt.args.token)
|
||||
got, err := tt.prov.authorizeToken(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
fmt.Println(tt)
|
||||
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
assert.Nil(t, got)
|
||||
} else {
|
||||
assert.NotNil(t, got)
|
||||
assert.Equals(t, got.Issuer, "the-issuer")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_AuthorizeSign(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
|
||||
var keys jose.JSONWebKeySet
|
||||
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
|
||||
|
||||
// Create test provisioners
|
||||
p1, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p2, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p3, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
// Admin + Domains
|
||||
p3.Admins = []string{"name@smallstep.com", "root@example.com"}
|
||||
p3.Domains = []string{"smallstep.com"}
|
||||
|
||||
// Update configuration endpoints and initialize
|
||||
config := Config{Claims: globalProvisionerClaims}
|
||||
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
p2.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
assert.FatalError(t, p1.Init(config))
|
||||
assert.FatalError(t, p2.Init(config))
|
||||
assert.FatalError(t, p3.Init(config))
|
||||
|
||||
t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// Admin email not in domains
|
||||
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// Invalid email
|
||||
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *OIDC
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok1", p1, args{t1}, false},
|
||||
{"admin", p3, args{okAdmin}, false},
|
||||
{"fail-email", p3, args{failEmail}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.prov.AuthorizeSign(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
fmt.Println(tt)
|
||||
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
@ -237,6 +296,63 @@ func TestOIDC_Authorize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOIDC_AuthorizeRevoke(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
|
||||
var keys jose.JSONWebKeySet
|
||||
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
|
||||
|
||||
// Create test provisioners
|
||||
p1, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p3, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
// Admin + Domains
|
||||
p3.Admins = []string{"name@smallstep.com", "root@example.com"}
|
||||
p3.Domains = []string{"smallstep.com"}
|
||||
|
||||
// Update configuration endpoints and initialize
|
||||
config := Config{Claims: globalProvisionerClaims}
|
||||
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
assert.FatalError(t, p1.Init(config))
|
||||
assert.FatalError(t, p3.Init(config))
|
||||
|
||||
t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// Admin email not in domains
|
||||
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// Invalid email
|
||||
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *OIDC
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok1", p1, args{t1}, true},
|
||||
{"admin", p3, args{okAdmin}, false},
|
||||
{"fail-email", p3, args{failEmail}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.prov.AuthorizeRevoke(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
fmt.Println(tt)
|
||||
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_AuthorizeRenewal(t *testing.T) {
|
||||
p1, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
|
@ -270,6 +386,7 @@ func TestOIDC_AuthorizeRenewal(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func TestOIDC_AuthorizeRevoke(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
|
@ -308,6 +425,7 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func Test_sanitizeEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
|
@ -11,15 +11,27 @@ import (
|
|||
// Interface is the interface that all provisioner types must implement.
|
||||
type Interface interface {
|
||||
GetID() string
|
||||
GetTokenID(token string) (string, error)
|
||||
GetName() string
|
||||
GetType() Type
|
||||
GetEncryptedKey() (kid string, key string, ok bool)
|
||||
Init(config Config) error
|
||||
Authorize(token string) ([]SignOption, error)
|
||||
AuthorizeSign(token string) ([]SignOption, error)
|
||||
AuthorizeRenewal(cert *x509.Certificate) error
|
||||
AuthorizeRevoke(token string) error
|
||||
}
|
||||
|
||||
// Audiences stores all supported audiences by request type.
|
||||
type Audiences struct {
|
||||
Sign []string
|
||||
Revoke []string
|
||||
}
|
||||
|
||||
// All returns all supported audiences across all request types in one list.
|
||||
func (a *Audiences) All() []string {
|
||||
return append(a.Sign, a.Revoke...)
|
||||
}
|
||||
|
||||
// Type indicates the provisioner Type.
|
||||
type Type int
|
||||
|
||||
|
@ -31,6 +43,11 @@ const (
|
|||
|
||||
// TypeOIDC is used to indicate the OIDC provisioners.
|
||||
TypeOIDC Type = 2
|
||||
|
||||
// RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map.
|
||||
RevokeAudienceKey = "revoke"
|
||||
// SignAudienceKey is the key for the 'sign' audiences in the audiences map.
|
||||
SignAudienceKey = "sign"
|
||||
)
|
||||
|
||||
// Config defines the default parameters used in the initialization of
|
||||
|
@ -39,7 +56,7 @@ type Config struct {
|
|||
// Claims are the default claims.
|
||||
Claims Claims
|
||||
// Audiences are the audiences used in the default provisioner, (JWK).
|
||||
Audiences []string
|
||||
Audiences Audiences
|
||||
}
|
||||
|
||||
type provisioner struct {
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
var testAudiences = []string{
|
||||
"https://ca.smallstep.com/sign",
|
||||
"https://ca.smallsteomcom/1.0/sign",
|
||||
var testAudiences = Audiences{
|
||||
Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"},
|
||||
Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"},
|
||||
}
|
||||
|
||||
func must(args ...interface{}) []interface{} {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -23,3 +24,14 @@ func (a *Authority) GetProvisioners(cursor string, limit int) (provisioner.List,
|
|||
provisioners, nextCursor := a.provisioners.Find(cursor, limit)
|
||||
return provisioners, nextCursor, nil
|
||||
}
|
||||
|
||||
// LoadProvisionerByCertificate returns an interface to the provisioner that
|
||||
// provisioned the certificate.
|
||||
func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisioner.Interface, error) {
|
||||
p, ok := a.provisioners.LoadByCertificate(crt)
|
||||
if !ok {
|
||||
return nil, &apiError{errors.Errorf("provisioner not found"),
|
||||
http.StatusNotFound, context{}}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ func TestRoot(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthority_GetRootCertificate(t *testing.T) {
|
||||
cert, err := pemutil.ReadCertificate("testdata/secrets/root_ca.crt")
|
||||
cert, err := pemutil.ReadCertificate("testdata/certs/root_ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ func TestAuthority_GetRootCertificate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthority_GetRootCertificates(t *testing.T) {
|
||||
cert, err := pemutil.ReadCertificate("testdata/secrets/root_ca.crt")
|
||||
cert, err := pemutil.ReadCertificate("testdata/certs/root_ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ func TestAuthority_GetRootCertificates(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthority_GetRoots(t *testing.T) {
|
||||
cert, err := pemutil.ReadCertificate("testdata/secrets/root_ca.crt")
|
||||
cert, err := pemutil.ReadCertificate("testdata/certs/root_ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func TestAuthority_GetRoots(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthority_GetFederation(t *testing.T) {
|
||||
cert, err := pemutil.ReadCertificate("testdata/secrets/root_ca.crt")
|
||||
cert, err := pemutil.ReadCertificate("testdata/certs/root_ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
14
authority/testdata/certs/foo.crt
vendored
Normal file
14
authority/testdata/certs/foo.crt
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICIDCCAcagAwIBAgIQTL7pKDl8mFzRziotXbgjEjAKBggqhkjOPQQDAjAnMSUw
|
||||
IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIy
|
||||
MjkyOVoXDTE5MDMyMzIyMjkyOVowHDEaMBgGA1UEAxMRZm9vLnNtYWxsc3RlcC5j
|
||||
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbptfDonFaeUPiTr52wl9r3dcz
|
||||
greolwDRmsgyFgnr1EuKH56WRcgH1gjfL0pybFlO3PdgBukR4u+sveq343OAo4He
|
||||
MIHbMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
|
||||
AwIwHQYDVR0OBBYEFP9pHiVlsx5mr4L2QirOb1G9Mo4jMB8GA1UdIwQYMBaAFKEe
|
||||
9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWZvby5zbWFsbHN0ZXAuY29t
|
||||
MEwGDCsGAQQBgqRkxihAAQQ8MDoCAQEECHN0ZXAtY2xpBCs0VUVMSng4ZTBhUzlt
|
||||
MENIM2ZaMEVCN0Q1YVVQSUNiNzU5ekFMSEZlanZjMAoGCCqGSM49BAMCA0gAMEUC
|
||||
IDxtNo1BX/4Sbf/+k1n+v//kh8ETr3clPvhjcyfvBIGTAiEAiT0kvbkPdCCnmHIw
|
||||
lhpgBwT5YReZzBwIYXyKyJXc07M=
|
||||
-----END CERTIFICATE-----
|
15
authority/testdata/certs/provisioner-not-found.crt
vendored
Normal file
15
authority/testdata/certs/provisioner-not-found.crt
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICTDCCAfGgAwIBAgIQH1JRmbStwdCkiuqf7SM8dzAKBggqhkjOPQQDAjAnMSUw
|
||||
IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIz
|
||||
MDI0OVoXDTE5MDMyMzIzMDI0OVowLjEsMCoGA1UEAxMjcHJvdmlzaW9uZXItbm90
|
||||
LWZvdW5kLnNtYWxsc3RlcC5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARw
|
||||
DOZEqgkXXY0PqnEvl5ADX4xXMDNgX4lraK8SP48Ljo3vUn5FqARjKaBgPLfowFkQ
|
||||
gnjsAbBPwzt4SUWZW0ybo4H3MIH0MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAU
|
||||
BggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFDLOyjWD26FV5lfIwPqegYIt
|
||||
PdmSMB8GA1UdIwQYMBaAFKEe9IdMyaHdURMjoJce7FN9HC9wMC4GA1UdEQQnMCWC
|
||||
I3Byb3Zpc2lvbmVyLW5vdC1mb3VuZC5zbWFsbHN0ZXAuY29tMFMGDCsGAQQBgqRk
|
||||
xihAAQRDMEECAQEED2dpZkBleGFtcGxlLmNvbQQrRVdDQThsdFJCdEwxN2VFQS1I
|
||||
dW4zQWtCN0sxTERhUXItNkdvdXc3RXBoVTAKBggqhkjOPQQDAgNJADBGAiEAkaHR
|
||||
dE706JI8eLio/AqPbH8A/qK1INlbKbrkZ03K5wECIQCqTGY4TYopJqLYt3HkQeTy
|
||||
cJfHpuPfIzvpT8X0h3zlwQ==
|
||||
-----END CERTIFICATE-----
|
14
authority/testdata/certs/renew-disabled.crt
vendored
Normal file
14
authority/testdata/certs/renew-disabled.crt
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICJjCCAcygAwIBAgIQWhtLLuWC1foM7eq1jefkGDAKBggqhkjOPQQDAjAnMSUw
|
||||
IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyNzIz
|
||||
Mzk0M1oXDTE5MDMyODIzMzk0M1owHDEaMBgGA1UEAxMRYmF6LnNtYWxsc3RlcC5j
|
||||
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATxC77uJiCHgxIoctoHZbEauQwV
|
||||
1FStMSKnEQwNkm88GD0HVUcz3g9OEHJbdMuY7VJjefD2NfdMil2N1jOw8VzMo4Hk
|
||||
MIHhMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
|
||||
AwIwHQYDVR0OBBYEFCEoFgFtPV3v3YsJt7uYoz7GgChEMB8GA1UdIwQYMBaAFKEe
|
||||
9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWJhei5zbWFsbHN0ZXAuY29t
|
||||
MFIGDCsGAQQBgqRkxihAAQRCMEACAQEEDnJlbmV3X2Rpc2FibGVkBCtJTWk5NFdC
|
||||
Tkk2Z1A1Y05IWGxaWU5VenZNakdkSHlCUm1Gb28tbENFYXFrMAoGCCqGSM49BAMC
|
||||
A0gAMEUCIQD1uGcIQYdEEtVtOFWZGhDk+QJTznH5C182k74Kj/Ns3QIgeNtqYeto
|
||||
Ur1bgN1pwEwjTyr4aNz+pUWHZhyodduVaCE=
|
||||
-----END CERTIFICATE-----
|
5
authority/testdata/secrets/foo.key
vendored
Normal file
5
authority/testdata/secrets/foo.key
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49
|
||||
AwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI
|
||||
B9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA==
|
||||
-----END EC PRIVATE KEY-----
|
5
authority/testdata/secrets/provisioner-not-found.key
vendored
Normal file
5
authority/testdata/secrets/provisioner-not-found.key
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEILWLnE+pkh9QQ0CcM89sCBAWMEK7EtoJOmHvvFpugj2joAoGCCqGSM49
|
||||
AwEHoUQDQgAEcAzmRKoJF12ND6pxL5eQA1+MVzAzYF+Ja2ivEj+PC46N71J+RagE
|
||||
YymgYDy36MBZEIJ47AGwT8M7eElFmVtMmw==
|
||||
-----END EC PRIVATE KEY-----
|
5
authority/testdata/secrets/renew-disabled.key
vendored
Normal file
5
authority/testdata/secrets/renew-disabled.key
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKmDvbNqeIZA9zssZxixJzAQBEUEBSyVnjCKvTWGMAd2oAoGCCqGSM49
|
||||
AwEHoUQDQgAE8Qu+7iYgh4MSKHLaB2WxGrkMFdRUrTEipxEMDZJvPBg9B1VHM94P
|
||||
ThByW3TLmO1SY3nw9jX3TIpdjdYzsPFczA==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -4,6 +4,7 @@ import (
|
|||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
|
@ -111,6 +113,13 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
|
|||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
|
||||
if err = a.db.StoreCertificate(serverCert); err != nil {
|
||||
if err != db.ErrNotImplemented {
|
||||
return nil, nil, &apiError{errors.Wrap(err, "sign: error storing certificate in db"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
}
|
||||
|
||||
return serverCert, caCert, nil
|
||||
}
|
||||
|
||||
|
@ -194,6 +203,80 @@ func (a *Authority) Renew(oldCert *x509.Certificate) (*x509.Certificate, *x509.C
|
|||
return serverCert, caCert, nil
|
||||
}
|
||||
|
||||
// RevokeOptions are the options for the Revoke API.
|
||||
type RevokeOptions struct {
|
||||
Serial string
|
||||
Reason string
|
||||
ReasonCode int
|
||||
PassiveOnly bool
|
||||
MTLS bool
|
||||
Crt *x509.Certificate
|
||||
OTT string
|
||||
errCtxt map[string]interface{}
|
||||
}
|
||||
|
||||
// Revoke revokes a certificate.
|
||||
//
|
||||
// NOTE: Only supports passive revocation - prevent existing certificates from
|
||||
// being renewed.
|
||||
//
|
||||
// TODO: Add OCSP and CRL support.
|
||||
func (a *Authority) Revoke(opts *RevokeOptions) error {
|
||||
errContext := context{
|
||||
"serialNumber": opts.Serial,
|
||||
"reasonCode": opts.ReasonCode,
|
||||
"reason": opts.Reason,
|
||||
"passiveOnly": opts.PassiveOnly,
|
||||
"mTLS": opts.MTLS,
|
||||
}
|
||||
if opts.MTLS {
|
||||
errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw)
|
||||
} else {
|
||||
errContext["ott"] = opts.OTT
|
||||
}
|
||||
|
||||
rci := &db.RevokedCertificateInfo{
|
||||
Serial: opts.Serial,
|
||||
ReasonCode: opts.ReasonCode,
|
||||
Reason: opts.Reason,
|
||||
MTLS: opts.MTLS,
|
||||
RevokedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
// Authorize mTLS or token request and get back a provisioner interface.
|
||||
p, err := a.authorizeRevoke(opts)
|
||||
if err != nil {
|
||||
return &apiError{errors.Wrap(err, "revoke"),
|
||||
http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
// If not mTLS then get the TokenID of the token.
|
||||
if !opts.MTLS {
|
||||
rci.TokenID, err = p.GetTokenID(opts.OTT)
|
||||
if err != nil {
|
||||
return &apiError{errors.Wrap(err, "revoke: could not get ID for token"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
errContext["tokenID"] = rci.TokenID
|
||||
}
|
||||
rci.ProvisionerID = p.GetID()
|
||||
errContext["provisionerID"] = rci.ProvisionerID
|
||||
|
||||
err = a.db.Revoke(rci)
|
||||
switch err {
|
||||
case nil:
|
||||
return nil
|
||||
case db.ErrNotImplemented:
|
||||
return &apiError{errors.New("revoke: no persistence layer configured"),
|
||||
http.StatusNotImplemented, errContext}
|
||||
case db.ErrAlreadyExists:
|
||||
return &apiError{errors.Errorf("revoke: certificate with serial number %s has already been revoked", rci.Serial),
|
||||
http.StatusBadRequest, errContext}
|
||||
default:
|
||||
return &apiError{err, http.StatusInternalServerError, errContext}
|
||||
}
|
||||
}
|
||||
|
||||
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
|
||||
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
||||
profile, err := x509util.NewLeafProfile("Step Online CA",
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
@ -15,10 +16,13 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/cli/crypto/keys"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
"github.com/smallstep/cli/jose"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -199,8 +203,36 @@ func TestSign(t *testing.T) {
|
|||
},
|
||||
}
|
||||
},
|
||||
"fail store cert in db": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv)
|
||||
_a := testAuthority(t)
|
||||
_a.db = &MockAuthDB{
|
||||
storeCertificate: func(crt *x509.Certificate) error {
|
||||
return &apiError{errors.New("force"),
|
||||
http.StatusInternalServerError,
|
||||
context{"csr": csr, "signOptions": signOpts}}
|
||||
},
|
||||
}
|
||||
return &signTest{
|
||||
auth: _a,
|
||||
csr: csr,
|
||||
extraOpts: extraOpts,
|
||||
signOpts: signOpts,
|
||||
err: &apiError{errors.New("sign: error storing certificate in db: force"),
|
||||
http.StatusInternalServerError,
|
||||
context{"csr": csr, "signOptions": signOpts},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv)
|
||||
_a := testAuthority(t)
|
||||
_a.db = &MockAuthDB{
|
||||
storeCertificate: func(crt *x509.Certificate) error {
|
||||
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return &signTest{
|
||||
auth: a,
|
||||
csr: csr,
|
||||
|
@ -350,7 +382,7 @@ func TestRenew(t *testing.T) {
|
|||
}
|
||||
return &renewTest{
|
||||
crt: crtNoRenew,
|
||||
err: &apiError{errors.New("renew is disabled for provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
|
||||
err: &apiError{errors.New("renew: renew is disabled for provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
|
||||
http.StatusUnauthorized, ctx},
|
||||
}, nil
|
||||
},
|
||||
|
@ -528,3 +560,230 @@ func TestGetTLSOptions(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
reasonCode := 2
|
||||
reason := "bob was let go"
|
||||
validIssuer := "step-cli"
|
||||
validAudience := []string{"https://test.ca.smallstep.com/revoke"}
|
||||
now := time.Now().UTC()
|
||||
getCtx := func() map[string]interface{} {
|
||||
return context{
|
||||
"serialNumber": "sn",
|
||||
"reasonCode": reasonCode,
|
||||
"reason": reason,
|
||||
"mTLS": false,
|
||||
"passiveOnly": false,
|
||||
}
|
||||
}
|
||||
|
||||
jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type test struct {
|
||||
a *Authority
|
||||
opts *RevokeOptions
|
||||
err *apiError
|
||||
}
|
||||
tests := map[string]func() test{
|
||||
"error/token/authorizeRevoke error": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = new(db.NoopDB)
|
||||
ctx := getCtx()
|
||||
ctx["ott"] = "foo"
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
OTT: "foo",
|
||||
Serial: "sn",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
},
|
||||
err: &apiError{errors.New("revoke: authorizeRevoke: authorizeToken: error parsing token"),
|
||||
http.StatusUnauthorized, ctx},
|
||||
}
|
||||
},
|
||||
"error/nil-db": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = new(db.NoopDB)
|
||||
|
||||
cl := jwt.Claims{
|
||||
Subject: "sn",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
ctx := getCtx()
|
||||
ctx["ott"] = raw
|
||||
ctx["tokenID"] = "44"
|
||||
ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
Serial: "sn",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
OTT: raw,
|
||||
},
|
||||
err: &apiError{errors.New("revoke: no persistence layer configured"),
|
||||
http.StatusNotImplemented, ctx},
|
||||
}
|
||||
},
|
||||
"error/db-revoke": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{err: errors.New("force")}
|
||||
|
||||
cl := jwt.Claims{
|
||||
Subject: "sn",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
ctx := getCtx()
|
||||
ctx["ott"] = raw
|
||||
ctx["tokenID"] = "44"
|
||||
ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
Serial: "sn",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
OTT: raw,
|
||||
},
|
||||
err: &apiError{errors.New("force"),
|
||||
http.StatusInternalServerError, ctx},
|
||||
}
|
||||
},
|
||||
"error/already-revoked": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{err: db.ErrAlreadyExists}
|
||||
|
||||
cl := jwt.Claims{
|
||||
Subject: "sn",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
ctx := getCtx()
|
||||
ctx["ott"] = raw
|
||||
ctx["tokenID"] = "44"
|
||||
ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
Serial: "sn",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
OTT: raw,
|
||||
},
|
||||
err: &apiError{errors.New("revoke: certificate with serial number sn has already been revoked"),
|
||||
http.StatusBadRequest, ctx},
|
||||
}
|
||||
},
|
||||
"ok/token": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{}
|
||||
|
||||
cl := jwt.Claims{
|
||||
Subject: "sn",
|
||||
Issuer: validIssuer,
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: validAudience,
|
||||
ID: "44",
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
Serial: "sn",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
OTT: raw,
|
||||
},
|
||||
}
|
||||
},
|
||||
"error/mTLS/authorizeRevoke": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{}
|
||||
|
||||
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
ctx := getCtx()
|
||||
ctx["certificate"] = base64.StdEncoding.EncodeToString(crt.Raw)
|
||||
ctx["mTLS"] = true
|
||||
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
Crt: crt,
|
||||
Serial: "sn",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
MTLS: true,
|
||||
},
|
||||
err: &apiError{errors.New("revoke: authorizeRevoke: serial number in certificate different than body"),
|
||||
http.StatusUnauthorized, ctx},
|
||||
}
|
||||
},
|
||||
"ok/mTLS": func() test {
|
||||
a := testAuthority(t)
|
||||
a.db = &MockAuthDB{}
|
||||
|
||||
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return test{
|
||||
a: a,
|
||||
opts: &RevokeOptions{
|
||||
Crt: crt,
|
||||
Serial: "102012593071130646873265215610956555026",
|
||||
ReasonCode: reasonCode,
|
||||
Reason: reason,
|
||||
MTLS: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, f := range tests {
|
||||
tc := f()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.a.Revoke(tc.opts); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch v := err.(type) {
|
||||
case *apiError:
|
||||
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.code, tc.err.code)
|
||||
assert.Equals(t, v.context, tc.err.context)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
3
ca/ca.go
3
ca/ca.go
|
@ -122,6 +122,9 @@ func (ca *CA) Run() error {
|
|||
// Stop stops the CA calling to the server Shutdown method.
|
||||
func (ca *CA) Stop() error {
|
||||
ca.renewer.Stop()
|
||||
if err := ca.auth.Shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
return ca.srv.Shutdown()
|
||||
}
|
||||
|
||||
|
|
30
ca/client.go
30
ca/client.go
|
@ -351,6 +351,36 @@ func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) {
|
|||
return &sign, nil
|
||||
}
|
||||
|
||||
// Revoke performs the revoke request to the CA and returns the api.RevokeResponse
|
||||
// struct.
|
||||
func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error marshaling request")
|
||||
}
|
||||
|
||||
var client *http.Client
|
||||
if tr != nil {
|
||||
client = &http.Client{Transport: tr}
|
||||
} else {
|
||||
client = c.client
|
||||
}
|
||||
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/revoke"})
|
||||
resp, err := client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "client POST %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, readError(resp.Body)
|
||||
}
|
||||
var revoke api.RevokeResponse
|
||||
if err := readJSON(resp.Body, &revoke); err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading %s", u)
|
||||
}
|
||||
return &revoke, nil
|
||||
}
|
||||
|
||||
// Provisioners performs the provisioners request to the CA and returns the
|
||||
// api.ProvisionersResponse struct with a map of provisioners.
|
||||
//
|
||||
|
|
|
@ -329,6 +329,81 @@ func TestClient_Sign(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Revoke(t *testing.T) {
|
||||
ok := &api.RevokeResponse{Status: "ok"}
|
||||
request := &api.RevokeRequest{
|
||||
Serial: "sn",
|
||||
OTT: "the-ott",
|
||||
ReasonCode: 4,
|
||||
}
|
||||
unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized"))
|
||||
badRequest := api.BadRequest(fmt.Errorf("Bad Request"))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
request *api.RevokeRequest
|
||||
response interface{}
|
||||
responseCode int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", request, ok, 200, false},
|
||||
{"unauthorized", request, unauthorized, 401, true},
|
||||
{"nil request", nil, badRequest, 403, true},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(nil)
|
||||
defer srv.Close()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))
|
||||
if err != nil {
|
||||
t.Errorf("NewClient() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body := new(api.RevokeRequest)
|
||||
if err := api.ReadJSON(req.Body, body); err != nil {
|
||||
api.WriteError(w, badRequest)
|
||||
return
|
||||
} else if !equalJSON(t, body, tt.request) {
|
||||
if tt.request == nil {
|
||||
if !reflect.DeepEqual(body, &api.RevokeRequest{}) {
|
||||
t.Errorf("Client.Revoke() request = %v, wants %v", body, tt.request)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Client.Revoke() request = %v, wants %v", body, tt.request)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(tt.responseCode)
|
||||
api.JSON(w, tt.response)
|
||||
})
|
||||
|
||||
got, err := c.Revoke(tt.request, nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
fmt.Printf("%+v", err)
|
||||
t.Errorf("Client.Revoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
if got != nil {
|
||||
t.Errorf("Client.Revoke() = %v, want nil", got)
|
||||
}
|
||||
if !reflect.DeepEqual(err, tt.response) {
|
||||
t.Errorf("Client.Revoke() error = %v, want %v", err, tt.response)
|
||||
}
|
||||
default:
|
||||
if !reflect.DeepEqual(got, tt.response) {
|
||||
t.Errorf("Client.Revoke() = %v, want %v", got, tt.response)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Renew(t *testing.T) {
|
||||
ok := &api.SignResponse{
|
||||
ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)},
|
||||
|
|
138
db/db.go
Normal file
138
db/db.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
var (
|
||||
revokedCertsTable = []byte("revoked_x509_certs")
|
||||
certsTable = []byte("x509_certs")
|
||||
)
|
||||
|
||||
// ErrAlreadyExists can be returned if the DB attempts to set a key that has
|
||||
// been previously set.
|
||||
var ErrAlreadyExists = errors.New("already exists")
|
||||
|
||||
// Config represents the JSON attributes used for configuring a step-ca DB.
|
||||
type Config struct {
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// AuthDB is an interface over an Authority DB client that implements a nosql.DB interface.
|
||||
type AuthDB interface {
|
||||
IsRevoked(sn string) (bool, error)
|
||||
Revoke(rci *RevokedCertificateInfo) error
|
||||
StoreCertificate(crt *x509.Certificate) error
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
// DB is a wrapper over the nosql.DB interface.
|
||||
type DB struct {
|
||||
nosql.DB
|
||||
}
|
||||
|
||||
// New returns a new database client that implements the AuthDB interface.
|
||||
func New(c *Config) (AuthDB, error) {
|
||||
if c == nil {
|
||||
return new(NoopDB), nil
|
||||
}
|
||||
|
||||
var db nosql.DB
|
||||
switch strings.ToLower(c.Type) {
|
||||
case "bbolt":
|
||||
db = &nosql.BoltDB{}
|
||||
if err := db.Open(c.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported db.type '%s'", c.Type)
|
||||
}
|
||||
|
||||
tables := [][]byte{revokedCertsTable, certsTable}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating table %s",
|
||||
string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
// RevokedCertificateInfo contains information regarding the certificate
|
||||
// revocation action.
|
||||
type RevokedCertificateInfo struct {
|
||||
Serial string
|
||||
ProvisionerID string
|
||||
ReasonCode int
|
||||
Reason string
|
||||
RevokedAt time.Time
|
||||
TokenID string
|
||||
MTLS bool
|
||||
}
|
||||
|
||||
// IsRevoked returns whether or not a certificate with the given identifier
|
||||
// has been revoked.
|
||||
// In the case of an X509 Certificate the `id` should be the Serial Number of
|
||||
// the Certificate.
|
||||
func (db *DB) IsRevoked(sn string) (bool, error) {
|
||||
// If the DB is nil then act as pass through.
|
||||
if db == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If the error is `Not Found` then the certificate has not been revoked.
|
||||
// Any other error should be propagated to the caller.
|
||||
if _, err := db.Get(revokedCertsTable, []byte(sn)); err != nil {
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.Wrap(err, "error checking revocation bucket")
|
||||
}
|
||||
|
||||
// This certificate has been revoked.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Revoke adds a certificate to the revocation table.
|
||||
func (db *DB) Revoke(rci *RevokedCertificateInfo) error {
|
||||
isRvkd, err := db.IsRevoked(rci.Serial)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isRvkd {
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
rcib, err := json.Marshal(rci)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error marshaling revoked certificate info")
|
||||
}
|
||||
|
||||
if err = db.Set(revokedCertsTable, []byte(rci.Serial), rcib); err != nil {
|
||||
return errors.Wrap(err, "database Set error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreCertificate stores a certificate PEM.
|
||||
func (db *DB) StoreCertificate(crt *x509.Certificate) error {
|
||||
if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil {
|
||||
return errors.Wrap(err, "database Set error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown sends a shutdown message to the database.
|
||||
func (db *DB) Shutdown() error {
|
||||
if err := db.Close(); err != nil {
|
||||
return errors.Wrap(err, "database shutdown error")
|
||||
}
|
||||
return nil
|
||||
}
|
190
db/db_test.go
Normal file
190
db/db_test.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type MockNoSQLDB struct {
|
||||
err error
|
||||
ret1, ret2 interface{}
|
||||
get func(bucket, key []byte) ([]byte, error)
|
||||
set func(bucket, key, value []byte) error
|
||||
open func(path string) error
|
||||
close func() error
|
||||
createTable func(bucket []byte) error
|
||||
deleteTable func(bucket []byte) error
|
||||
del func(bucket, key []byte) error
|
||||
list func(bucket []byte) ([]*nosql.Entry, error)
|
||||
update func(tx *nosql.Tx) error
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) Get(bucket, key []byte) ([]byte, error) {
|
||||
if m.get != nil {
|
||||
return m.get(bucket, key)
|
||||
}
|
||||
if m.ret1 == nil {
|
||||
return nil, m.err
|
||||
}
|
||||
return m.ret1.([]byte), m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) Set(bucket, key, value []byte) error {
|
||||
if m.set != nil {
|
||||
return m.set(bucket, key, value)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) Open(path string) error {
|
||||
if m.open != nil {
|
||||
return m.open(path)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) Close() error {
|
||||
if m.close != nil {
|
||||
return m.close()
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) CreateTable(bucket []byte) error {
|
||||
if m.createTable != nil {
|
||||
return m.createTable(bucket)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) DeleteTable(bucket []byte) error {
|
||||
if m.deleteTable != nil {
|
||||
return m.deleteTable(bucket)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) Del(bucket, key []byte) error {
|
||||
if m.del != nil {
|
||||
return m.del(bucket, key)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) List(bucket []byte) ([]*nosql.Entry, error) {
|
||||
if m.list != nil {
|
||||
return m.list(bucket)
|
||||
}
|
||||
return m.ret1.([]*nosql.Entry), m.err
|
||||
}
|
||||
|
||||
func (m *MockNoSQLDB) Update(tx *nosql.Tx) error {
|
||||
if m.update != nil {
|
||||
return m.update(tx)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func TestIsRevoked(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
key string
|
||||
db *DB
|
||||
isRevoked bool
|
||||
err error
|
||||
}{
|
||||
"false/nil db": {
|
||||
key: "sn",
|
||||
},
|
||||
"false/ErrNotFound": {
|
||||
key: "sn",
|
||||
db: &DB{&MockNoSQLDB{err: nosql.ErrNotFound, ret1: nil}},
|
||||
},
|
||||
"error/checking bucket": {
|
||||
key: "sn",
|
||||
db: &DB{&MockNoSQLDB{err: errors.New("force"), ret1: nil}},
|
||||
err: errors.New("error checking revocation bucket: force"),
|
||||
},
|
||||
"true": {
|
||||
key: "sn",
|
||||
db: &DB{&MockNoSQLDB{ret1: []byte("value")}},
|
||||
isRevoked: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
isRevoked, err := tc.db.IsRevoked(tc.key)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, tc.err.Error(), err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
assert.Fatal(t, isRevoked == tc.isRevoked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
rci *RevokedCertificateInfo
|
||||
db *DB
|
||||
err error
|
||||
}{
|
||||
"error/force isRevoked": {
|
||||
rci: &RevokedCertificateInfo{Serial: "sn"},
|
||||
db: &DB{&MockNoSQLDB{
|
||||
get: func(bucket []byte, sn []byte) ([]byte, error) {
|
||||
return nil, errors.New("force IsRevoked")
|
||||
},
|
||||
}},
|
||||
err: errors.New("error checking revocation bucket: force IsRevoked"),
|
||||
},
|
||||
"error/was already revoked": {
|
||||
rci: &RevokedCertificateInfo{Serial: "sn"},
|
||||
db: &DB{&MockNoSQLDB{
|
||||
get: func(bucket []byte, sn []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}},
|
||||
err: ErrAlreadyExists,
|
||||
},
|
||||
"error/database set": {
|
||||
rci: &RevokedCertificateInfo{Serial: "sn"},
|
||||
db: &DB{&MockNoSQLDB{
|
||||
get: func(bucket []byte, sn []byte) ([]byte, error) {
|
||||
return nil, nosql.ErrNotFound
|
||||
},
|
||||
set: func(bucket []byte, key []byte, value []byte) error {
|
||||
return errors.New("force")
|
||||
},
|
||||
}},
|
||||
err: errors.New("database Set error: force"),
|
||||
},
|
||||
"ok": {
|
||||
rci: &RevokedCertificateInfo{Serial: "sn"},
|
||||
db: &DB{&MockNoSQLDB{
|
||||
get: func(bucket []byte, sn []byte) ([]byte, error) {
|
||||
return nil, nosql.ErrNotFound
|
||||
},
|
||||
set: func(bucket []byte, key []byte, value []byte) error {
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.db.Revoke(tc.rci); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, tc.err.Error(), err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
38
db/noop.go
Normal file
38
db/noop.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
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
|
||||
}
|
21
db/noop_test.go
Normal file
21
db/noop_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
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)
|
||||
}
|
Loading…
Reference in a new issue