Add /revoke API with interface db backend
parent
07ff7d9807
commit
ab4d569f36
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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-----
|
@ -0,0 +1,15 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICTDCCAfGgAwIBAgIQH1JRmbStwdCkiuqf7SM8dzAKBggqhkjOPQQDAjAnMSUw
|
||||||
|
IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIz
|
||||||
|
MDI0OVoXDTE5MDMyMzIzMDI0OVowLjEsMCoGA1UEAxMjcHJvdmlzaW9uZXItbm90
|
||||||
|
LWZvdW5kLnNtYWxsc3RlcC5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARw
|
||||||
|
DOZEqgkXXY0PqnEvl5ADX4xXMDNgX4lraK8SP48Ljo3vUn5FqARjKaBgPLfowFkQ
|
||||||
|
gnjsAbBPwzt4SUWZW0ybo4H3MIH0MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAU
|
||||||
|
BggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFDLOyjWD26FV5lfIwPqegYIt
|
||||||
|
PdmSMB8GA1UdIwQYMBaAFKEe9IdMyaHdURMjoJce7FN9HC9wMC4GA1UdEQQnMCWC
|
||||||
|
I3Byb3Zpc2lvbmVyLW5vdC1mb3VuZC5zbWFsbHN0ZXAuY29tMFMGDCsGAQQBgqRk
|
||||||
|
xihAAQRDMEECAQEED2dpZkBleGFtcGxlLmNvbQQrRVdDQThsdFJCdEwxN2VFQS1I
|
||||||
|
dW4zQWtCN0sxTERhUXItNkdvdXc3RXBoVTAKBggqhkjOPQQDAgNJADBGAiEAkaHR
|
||||||
|
dE706JI8eLio/AqPbH8A/qK1INlbKbrkZ03K5wECIQCqTGY4TYopJqLYt3HkQeTy
|
||||||
|
cJfHpuPfIzvpT8X0h3zlwQ==
|
||||||
|
-----END CERTIFICATE-----
|
@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICJjCCAcygAwIBAgIQWhtLLuWC1foM7eq1jefkGDAKBggqhkjOPQQDAjAnMSUw
|
||||||
|
IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyNzIz
|
||||||
|
Mzk0M1oXDTE5MDMyODIzMzk0M1owHDEaMBgGA1UEAxMRYmF6LnNtYWxsc3RlcC5j
|
||||||
|
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATxC77uJiCHgxIoctoHZbEauQwV
|
||||||
|
1FStMSKnEQwNkm88GD0HVUcz3g9OEHJbdMuY7VJjefD2NfdMil2N1jOw8VzMo4Hk
|
||||||
|
MIHhMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
|
||||||
|
AwIwHQYDVR0OBBYEFCEoFgFtPV3v3YsJt7uYoz7GgChEMB8GA1UdIwQYMBaAFKEe
|
||||||
|
9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWJhei5zbWFsbHN0ZXAuY29t
|
||||||
|
MFIGDCsGAQQBgqRkxihAAQRCMEACAQEEDnJlbmV3X2Rpc2FibGVkBCtJTWk5NFdC
|
||||||
|
Tkk2Z1A1Y05IWGxaWU5VenZNakdkSHlCUm1Gb28tbENFYXFrMAoGCCqGSM49BAMC
|
||||||
|
A0gAMEUCIQD1uGcIQYdEEtVtOFWZGhDk+QJTznH5C182k74Kj/Ns3QIgeNtqYeto
|
||||||
|
Ur1bgN1pwEwjTyr4aNz+pUWHZhyodduVaCE=
|
||||||
|
-----END CERTIFICATE-----
|
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI
|
||||||
|
B9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEILWLnE+pkh9QQ0CcM89sCBAWMEK7EtoJOmHvvFpugj2joAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEcAzmRKoJF12ND6pxL5eQA1+MVzAzYF+Ja2ivEj+PC46N71J+RagE
|
||||||
|
YymgYDy36MBZEIJ47AGwT8M7eElFmVtMmw==
|
||||||
|
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIKmDvbNqeIZA9zssZxixJzAQBEUEBSyVnjCKvTWGMAd2oAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAE8Qu+7iYgh4MSKHLaB2WxGrkMFdRUrTEipxEMDZJvPBg9B1VHM94P
|
||||||
|
ThByW3TLmO1SY3nw9jX3TIpdjdYzsPFczA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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 New Issue