forked from TrueCloudLab/certificates
Merge branch 'master' into herman/allow-deny
This commit is contained in:
commit
c3c6f3da72
36 changed files with 725 additions and 969 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
version: 'latest'
|
version: 'v1.44.0'
|
||||||
|
|
||||||
# Optional: working directory, useful for monorepos
|
# Optional: working directory, useful for monorepos
|
||||||
# working-directory: somedir
|
# working-directory: somedir
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
version: 'v1.43.0'
|
version: 'v1.44.0'
|
||||||
|
|
||||||
# Optional: working directory, useful for monorepos
|
# Optional: working directory, useful for monorepos
|
||||||
# working-directory: somedir
|
# working-directory: somedir
|
||||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -4,16 +4,31 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased - 0.18.1] - DATE
|
## [Unreleased - 0.18.2] - DATE
|
||||||
### Added
|
### Added
|
||||||
- Support for ACME revocation.
|
|
||||||
- Replace hash function with an RSA SSH CA to "rsa-sha2-256".
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- IPv6 addresses are normalized as IP addresses instead of hostnames.
|
||||||
|
- More descriptive JWK decryption error message.
|
||||||
|
- Make the X5C leaf certificate available to the templates using `{{ .AuthorizationCrt }}`.
|
||||||
### Deprecated
|
### Deprecated
|
||||||
### Removed
|
### Removed
|
||||||
### Fixed
|
### Fixed
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
## [0.18.1] - 2022-02-03
|
||||||
|
### Added
|
||||||
|
- Support for ACME revocation.
|
||||||
|
- Replace hash function with an RSA SSH CA to "rsa-sha2-256".
|
||||||
|
- Support Nebula provisioners.
|
||||||
|
- Example Ansible configurations.
|
||||||
|
- Support PKCS#11 as a decrypter, as used by SCEP.
|
||||||
|
### Changed
|
||||||
|
- Automatically create database directory on `step ca init`.
|
||||||
|
- Slightly improve errors reported when a template has invalid content.
|
||||||
|
- Error reporting in logs and to clients.
|
||||||
|
### Fixed
|
||||||
|
- SCEP renewal using HTTPS on macOS.
|
||||||
|
|
||||||
## [0.18.0] - 2021-11-17
|
## [0.18.0] - 2021-11-17
|
||||||
### Added
|
### Added
|
||||||
- Support for multiple certificate authority contexts.
|
- Support for multiple certificate authority contexts.
|
||||||
|
|
|
@ -3,13 +3,29 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewLinker returns a new Directory type.
|
// NewLinker returns a new Directory type.
|
||||||
func NewLinker(dns, prefix string) Linker {
|
func NewLinker(dns, prefix string) Linker {
|
||||||
|
_, _, err := net.SplitHostPort(dns)
|
||||||
|
if err != nil && strings.Contains(err.Error(), "too many colons in address") {
|
||||||
|
// this is most probably an IPv6 without brackets, e.g. ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||||
|
// in case a port was appended to this wrong format, we try to extract the port, then check if it's
|
||||||
|
// still a valid IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443 (8443 is the port). If none of
|
||||||
|
// these cases, then the input dns is not changed.
|
||||||
|
lastIndex := strings.LastIndex(dns, ":")
|
||||||
|
hostPart, portPart := dns[:lastIndex], dns[lastIndex+1:]
|
||||||
|
if ip := net.ParseIP(hostPart); ip != nil {
|
||||||
|
dns = "[" + hostPart + "]:" + portPart
|
||||||
|
} else if ip := net.ParseIP(dns); ip != nil {
|
||||||
|
dns = "[" + dns + "]"
|
||||||
|
}
|
||||||
|
}
|
||||||
return &linker{prefix: prefix, dns: dns}
|
return &linker{prefix: prefix, dns: dns}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,86 @@ func TestLinker_GetUnescapedPathSuffix(t *testing.T) {
|
||||||
assert.Equals(t, getPath(CertificateLinkType, "{provisionerID}", "{certID}"), "/{provisionerID}/certificate/{certID}")
|
assert.Equals(t, getPath(CertificateLinkType, "{provisionerID}", "{certID}"), "/{provisionerID}/certificate/{certID}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLinker_DNS(t *testing.T) {
|
||||||
|
prov := newProv()
|
||||||
|
escProvName := url.PathEscape(prov.GetName())
|
||||||
|
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
dns string
|
||||||
|
prefix string
|
||||||
|
expectedDirectoryLink string
|
||||||
|
}
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "domain",
|
||||||
|
dns: "ca.smallstep.com",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://ca.smallstep.com/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "domain-port",
|
||||||
|
dns: "ca.smallstep.com:8443",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://ca.smallstep.com:8443/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4",
|
||||||
|
dns: "127.0.0.1",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://127.0.0.1/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4-port",
|
||||||
|
dns: "127.0.0.1:8443",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://127.0.0.1:8443/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6",
|
||||||
|
dns: "[::1]",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://[::1]/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6-port",
|
||||||
|
dns: "[::1]:8443",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://[::1]:8443/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6-no-brackets",
|
||||||
|
dns: "::1",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://[::1]/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6-port-no-brackets",
|
||||||
|
dns: "::1:8443",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://[::1]:8443/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6-long-no-brackets",
|
||||||
|
dns: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6-long-port-no-brackets",
|
||||||
|
dns: "2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443",
|
||||||
|
prefix: "acme",
|
||||||
|
expectedDirectoryLink: fmt.Sprintf("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8443/acme/%s/directory", escProvName),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
linker := NewLinker(tt.dns, tt.prefix)
|
||||||
|
assert.Equals(t, tt.expectedDirectoryLink, linker.GetLink(ctx, DirectoryLinkType))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLinker_GetLink(t *testing.T) {
|
func TestLinker_GetLink(t *testing.T) {
|
||||||
dns := "ca.smallstep.com"
|
dns := "ca.smallstep.com"
|
||||||
prefix := "acme"
|
prefix := "acme"
|
||||||
|
|
|
@ -2,17 +2,14 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/smallstep/certificates/acme"
|
|
||||||
"github.com/smallstep/certificates/api"
|
"github.com/smallstep/certificates/api"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -70,7 +67,7 @@ func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName
|
||||||
return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName)
|
return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
prov, err := h.db.GetProvisioner(ctx, p.GetID())
|
prov, err := h.adminDB.GetProvisioner(ctx, p.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID())
|
return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID())
|
||||||
}
|
}
|
||||||
|
@ -88,159 +85,31 @@ func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName
|
||||||
return acmeProvisioner.GetRequireEab(), prov, nil
|
return acmeProvisioner.GetRequireEab(), prov, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionerFromContext searches the context for a provisioner. Returns the
|
type acmeAdminResponderInterface interface {
|
||||||
// provisioner or an error.
|
GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
||||||
func provisionerFromContext(ctx context.Context) (*linkedca.Provisioner, error) {
|
CreateExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||||
val := ctx.Value(provisionerContextKey)
|
DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||||
if val == nil {
|
|
||||||
return nil, admin.NewErrorISE("provisioner expected in request context")
|
|
||||||
}
|
|
||||||
pval, ok := val.(*linkedca.Provisioner)
|
|
||||||
if !ok || pval == nil {
|
|
||||||
return nil, admin.NewErrorISE("provisioner in context is not a linkedca.Provisioner")
|
|
||||||
}
|
|
||||||
return pval, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateExternalAccountKey creates a new External Account Binding key
|
// ACMEAdminResponder is responsible for writing ACME admin responses
|
||||||
func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
type ACMEAdminResponder struct{}
|
||||||
var body CreateExternalAccountKeyRequest
|
|
||||||
if err := api.ReadJSON(r.Body, &body); err != nil {
|
|
||||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := body.Validate(); err != nil {
|
// NewACMEAdminResponder returns a new ACMEAdminResponder
|
||||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating request body"))
|
func NewACMEAdminResponder() *ACMEAdminResponder {
|
||||||
return
|
return &ACMEAdminResponder{}
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
prov, err := provisionerFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a key with the reference does not exist (only when a reference was in the request)
|
|
||||||
reference := body.Reference
|
|
||||||
if reference != "" {
|
|
||||||
k, err := h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference)
|
|
||||||
// retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for,
|
|
||||||
// but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound.
|
|
||||||
if shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound); shouldWriteError {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "could not lookup external account key by reference"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// if a key was found, return HTTP 409 conflict
|
|
||||||
if k != nil {
|
|
||||||
err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner '%s' with reference '%s' already exists", prov.GetName(), reference)
|
|
||||||
err.Status = 409
|
|
||||||
api.WriteError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// continue execution if no key was found for the reference
|
|
||||||
}
|
|
||||||
|
|
||||||
eak, err := h.acmeDB.CreateExternalAccountKey(ctx, prov.GetId(), reference)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov.GetName())
|
|
||||||
if reference != "" {
|
|
||||||
msg += fmt.Sprintf(" and reference '%s'", reference)
|
|
||||||
}
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := &linkedca.EABKey{
|
|
||||||
Id: eak.ID,
|
|
||||||
HmacKey: eak.KeyBytes,
|
|
||||||
Provisioner: prov.GetName(),
|
|
||||||
Reference: eak.Reference,
|
|
||||||
}
|
|
||||||
|
|
||||||
api.ProtoJSONStatus(w, response, http.StatusCreated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteExternalAccountKey deletes an ACME External Account Key.
|
// GetExternalAccountKeys writes the response for the EAB keys GET endpoint
|
||||||
func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
func (h *ACMEAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||||
keyID := chi.URLParam(r, "id")
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
prov, err := provisionerFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.acmeDB.DeleteExternalAccountKey(ctx, prov.GetId(), keyID); err != nil {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.JSON(w, &DeleteResponse{Status: "ok"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExternalAccountKeys returns ACME EAB Keys. If a reference is specified,
|
// CreateExternalAccountKey writes the response for the EAB key POST endpoint
|
||||||
// only the ExternalAccountKey with that reference is returned. Otherwise all
|
func (h *ACMEAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||||
// ExternalAccountKeys in the system for a specific provisioner are returned.
|
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||||
func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
|
}
|
||||||
|
|
||||||
var (
|
// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint
|
||||||
key *acme.ExternalAccountKey
|
func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||||
keys []*acme.ExternalAccountKey
|
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||||
err error
|
|
||||||
cursor string
|
|
||||||
nextCursor string
|
|
||||||
limit int
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
prov, err := provisionerFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cursor, limit, err = api.ParseCursor(r); err != nil {
|
|
||||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err,
|
|
||||||
"error parsing cursor and limit from query params"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reference := chi.URLParam(r, "reference")
|
|
||||||
if reference != "" {
|
|
||||||
if key, err = h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference); err != nil {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if key != nil {
|
|
||||||
keys = []*acme.ExternalAccountKey{key}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId(), cursor, limit); err != nil {
|
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provisionerName := prov.GetName()
|
|
||||||
eaks := make([]*linkedca.EABKey, len(keys))
|
|
||||||
for i, k := range keys {
|
|
||||||
eaks[i] = &linkedca.EABKey{
|
|
||||||
Id: k.ID,
|
|
||||||
HmacKey: []byte{},
|
|
||||||
Provisioner: provisionerName,
|
|
||||||
Reference: k.Reference,
|
|
||||||
Account: k.AccountID,
|
|
||||||
CreatedAt: timestamppb.New(k.CreatedAt),
|
|
||||||
BoundAt: timestamppb.New(k.BoundAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.JSON(w, &GetExternalAccountKeysResponse{
|
|
||||||
EAKs: eaks,
|
|
||||||
NextCursor: nextCursor,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,19 +10,14 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
||||||
|
@ -37,7 +32,7 @@ func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
||||||
func TestHandler_requireEABEnabled(t *testing.T) {
|
func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
db admin.DB
|
adminDB admin.DB
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
next nextHTTP
|
next nextHTTP
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
|
@ -98,7 +93,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
err: err,
|
err: err,
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
}
|
}
|
||||||
|
@ -136,7 +131,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
next: func(w http.ResponseWriter, r *http.Request) {
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(nil) // mock response with status 200
|
w.Write(nil) // mock response with status 200
|
||||||
},
|
},
|
||||||
|
@ -149,8 +144,8 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
db: tc.db,
|
|
||||||
auth: tc.auth,
|
auth: tc.auth,
|
||||||
|
adminDB: tc.adminDB,
|
||||||
acmeDB: nil,
|
acmeDB: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +178,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
|
|
||||||
func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
db admin.DB
|
adminDB admin.DB
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
provisionerName string
|
provisionerName string
|
||||||
want bool
|
want bool
|
||||||
|
@ -223,7 +218,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
provisionerName: "provName",
|
provisionerName: "provName",
|
||||||
want: false,
|
want: false,
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
||||||
|
@ -252,7 +247,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
provisionerName: "provName",
|
provisionerName: "provName",
|
||||||
want: false,
|
want: false,
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
||||||
|
@ -285,7 +280,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
provisionerName: "provName",
|
provisionerName: "provName",
|
||||||
want: false,
|
want: false,
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
||||||
|
@ -319,7 +314,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
db: db,
|
adminDB: db,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
provisionerName: "eab-disabled",
|
provisionerName: "eab-disabled",
|
||||||
want: false,
|
want: false,
|
||||||
|
@ -353,7 +348,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
db: db,
|
adminDB: db,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
provisionerName: "eab-enabled",
|
provisionerName: "eab-enabled",
|
||||||
want: true,
|
want: true,
|
||||||
|
@ -364,8 +359,8 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
db: tc.db,
|
|
||||||
auth: tc.auth,
|
auth: tc.auth,
|
||||||
|
adminDB: tc.adminDB,
|
||||||
acmeDB: nil,
|
acmeDB: nil,
|
||||||
}
|
}
|
||||||
got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName)
|
got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName)
|
||||||
|
@ -391,54 +386,6 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_provisionerFromContext(t *testing.T) {
|
|
||||||
prov := &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "acmeProv",
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ctx context.Context
|
|
||||||
want *linkedca.Provisioner
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "fail/no-provisioner",
|
|
||||||
ctx: context.Background(),
|
|
||||||
want: nil,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fail/wrong-type",
|
|
||||||
ctx: context.WithValue(context.Background(), provisionerContextKey, "prov"),
|
|
||||||
want: nil,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ok",
|
|
||||||
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
|
||||||
want: &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "acmeProv",
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := provisionerFromContext(tt.ctx)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("provisionerFromContext() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{})}
|
|
||||||
if !cmp.Equal(tt.want, got, opts...) {
|
|
||||||
t.Errorf("provisionerFromContext() diff =\n %s", cmp.Diff(tt.want, got, opts...))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Reference string
|
Reference string
|
||||||
|
@ -483,311 +430,39 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_CreateExternalAccountKey(t *testing.T) {
|
func TestHandler_CreateExternalAccountKey(t *testing.T) {
|
||||||
prov := &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
}
|
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
db acme.DB
|
|
||||||
body []byte
|
|
||||||
statusCode int
|
statusCode int
|
||||||
eak *linkedca.EABKey
|
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
}
|
}
|
||||||
var tests = map[string]func(t *testing.T) test{
|
var tests = map[string]func(t *testing.T) test{
|
||||||
"fail/ReadJSON": func(t *testing.T) test {
|
"ok": func(t *testing.T) test {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
body := []byte("{!?}")
|
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
statusCode: 501,
|
||||||
statusCode: 400,
|
|
||||||
eak: nil,
|
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorBadRequestType.String(),
|
Type: admin.ErrorNotImplementedType.String(),
|
||||||
Status: 400,
|
Status: http.StatusNotImplemented,
|
||||||
Detail: "bad request",
|
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
||||||
Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string",
|
Detail: "not implemented",
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/validate": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: strings.Repeat("A", 257),
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
body: body,
|
|
||||||
statusCode: 400,
|
|
||||||
eak: nil,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorBadRequestType.String(),
|
|
||||||
Status: 400,
|
|
||||||
Detail: "bad request",
|
|
||||||
Message: "error validating request body: reference length 257 exceeds the maximum (256)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/no-provisioner-in-context": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "aRef",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
body: body,
|
|
||||||
statusCode: 500,
|
|
||||||
eak: nil,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Status: 500,
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error getting provisioner from context: provisioner expected in request context",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
body: body,
|
|
||||||
statusCode: 500,
|
|
||||||
eak: nil,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Status: 500,
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "could not lookup external account key by reference: force",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/reference-conflict-409": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
past := time.Now().Add(-24 * time.Hour)
|
|
||||||
return &acme.ExternalAccountKey{
|
|
||||||
ID: "eakID",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
|
||||||
CreatedAt: past,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
body: body,
|
|
||||||
statusCode: 409,
|
|
||||||
eak: nil,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorBadRequestType.String(),
|
|
||||||
Status: 409,
|
|
||||||
Detail: "bad request",
|
|
||||||
Message: "an ACME EAB key for provisioner 'provName' with reference 'an-external-key-reference' already exists",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/acmeDB.CreateExternalAccountKey-no-reference": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "", reference)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
body: body,
|
|
||||||
statusCode: 500,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Status: 500,
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error creating ACME EAB key for provisioner 'provName': force",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/acmeDB.CreateExternalAccountKey-with-reference": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
|
|
||||||
},
|
|
||||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
body: body,
|
|
||||||
statusCode: 500,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Status: 500,
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error creating ACME EAB key for provisioner 'provName' and reference 'an-external-key-reference': force",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/no-reference": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
now := time.Now()
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "", reference)
|
|
||||||
return &acme.ExternalAccountKey{
|
|
||||||
ID: "eakID",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "",
|
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
|
||||||
CreatedAt: now,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
body: body,
|
|
||||||
statusCode: 201,
|
|
||||||
eak: &linkedca.EABKey{
|
|
||||||
Id: "eakID",
|
|
||||||
Provisioner: "provName",
|
|
||||||
Reference: "",
|
|
||||||
HmacKey: []byte{1, 3, 3, 7},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/with-reference": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
req := CreateExternalAccountKeyRequest{
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(req)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
now := time.Now()
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
|
|
||||||
},
|
|
||||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return &acme.ExternalAccountKey{
|
|
||||||
ID: "eakID",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
|
||||||
CreatedAt: now,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
body: body,
|
|
||||||
statusCode: 201,
|
|
||||||
eak: &linkedca.EABKey{
|
|
||||||
Id: "eakID",
|
|
||||||
Provisioner: "provName",
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
HmacKey: []byte{1, 3, 3, 7},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, prep := range tests {
|
for name, prep := range tests {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
|
||||||
acmeDB: tc.db,
|
req := httptest.NewRequest("POST", "/foo", nil) // chi routing is prepared in test setup
|
||||||
}
|
|
||||||
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) // chi routing is prepared in test setup
|
|
||||||
req = req.WithContext(tc.ctx)
|
req = req.WithContext(tc.ctx)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.CreateExternalAccountKey(w, req)
|
acmeResponder := NewACMEAdminResponder()
|
||||||
|
acmeResponder.CreateExternalAccountKey(w, req)
|
||||||
res := w.Result()
|
res := w.Result()
|
||||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||||
|
|
||||||
if res.StatusCode >= 400 {
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -800,110 +475,47 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
|
||||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
eabKey := &linkedca.EABKey{}
|
|
||||||
err := readProtoJSON(res.Body, eabKey)
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
|
||||||
|
|
||||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{})}
|
|
||||||
if !cmp.Equal(tc.eak, eabKey, opts...) {
|
|
||||||
t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.eak, eabKey, opts...))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_DeleteExternalAccountKey(t *testing.T) {
|
func TestHandler_DeleteExternalAccountKey(t *testing.T) {
|
||||||
prov := &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
}
|
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
db acme.DB
|
|
||||||
statusCode int
|
statusCode int
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
}
|
}
|
||||||
var tests = map[string]func(t *testing.T) test{
|
var tests = map[string]func(t *testing.T) test{
|
||||||
"fail/no-provisioner-in-context": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 500,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Status: 500,
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error getting provisioner from context: provisioner expected in request context",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
chiCtx.URLParams.Add("id", "keyID")
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "keyID", keyID)
|
|
||||||
return errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
statusCode: 500,
|
|
||||||
err: &admin.Error{
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Status: 500,
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error deleting ACME EAB Key 'keyID': force",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok": func(t *testing.T) test {
|
"ok": func(t *testing.T) test {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||||
chiCtx.URLParams.Add("id", "keyID")
|
chiCtx.URLParams.Add("id", "keyID")
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "keyID", keyID)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
db: db,
|
statusCode: 501,
|
||||||
statusCode: 200,
|
err: &admin.Error{
|
||||||
err: nil,
|
Type: admin.ErrorNotImplementedType.String(),
|
||||||
|
Status: http.StatusNotImplemented,
|
||||||
|
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
||||||
|
Detail: "not implemented",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for name, prep := range tests {
|
for name, prep := range tests {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
|
||||||
acmeDB: tc.db,
|
|
||||||
}
|
|
||||||
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
|
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
|
||||||
req = req.WithContext(tc.ctx)
|
req = req.WithContext(tc.ctx)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.DeleteExternalAccountKey(w, req)
|
acmeResponder := NewACMEAdminResponder()
|
||||||
|
acmeResponder.DeleteExternalAccountKey(w, req)
|
||||||
res := w.Result()
|
res := w.Result()
|
||||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||||
|
|
||||||
if res.StatusCode >= 400 {
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -916,279 +528,48 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
|
||||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
|
|
||||||
response := DeleteResponse{}
|
|
||||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
|
||||||
assert.Equals(t, "ok", response.Status)
|
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
||||||
prov := &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
}
|
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
db acme.DB
|
|
||||||
statusCode int
|
statusCode int
|
||||||
req *http.Request
|
req *http.Request
|
||||||
resp GetExternalAccountKeysResponse
|
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
}
|
}
|
||||||
var tests = map[string]func(t *testing.T) test{
|
var tests = map[string]func(t *testing.T) test{
|
||||||
"fail/no-provisioner-in-context": func(t *testing.T) test {
|
"ok": func(t *testing.T) test {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil)
|
req := httptest.NewRequest("GET", "/foo", nil)
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 500,
|
statusCode: 501,
|
||||||
req: req,
|
req: req,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorNotImplementedType.String(),
|
||||||
Status: 500,
|
Status: http.StatusNotImplemented,
|
||||||
Detail: "the server experienced an internal error",
|
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
||||||
Message: "error getting provisioner from context: provisioner expected in request context",
|
Detail: "not implemented",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fail/parse-cursor": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
req := httptest.NewRequest("GET", "/foo?limit=A", nil)
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 400,
|
|
||||||
req: req,
|
|
||||||
err: &admin.Error{
|
|
||||||
Status: 400,
|
|
||||||
Type: admin.ErrorBadRequestType.String(),
|
|
||||||
Detail: "bad request",
|
|
||||||
Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
chiCtx.URLParams.Add("reference", "an-external-key-reference")
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil)
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 500,
|
|
||||||
req: req,
|
|
||||||
db: db,
|
|
||||||
err: &admin.Error{
|
|
||||||
Status: 500,
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error retrieving external account key with reference 'an-external-key-reference': force",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/acmeDB.GetExternalAccountKeys": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil)
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "", cursor)
|
|
||||||
assert.Equals(t, 0, limit)
|
|
||||||
return nil, "", errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 500,
|
|
||||||
req: req,
|
|
||||||
db: db,
|
|
||||||
err: &admin.Error{
|
|
||||||
Status: 500,
|
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
|
||||||
Detail: "the server experienced an internal error",
|
|
||||||
Message: "error retrieving external account keys: force",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/reference-not-found": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
chiCtx.URLParams.Add("reference", "an-external-key-reference")
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil)
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return nil, nil // returning nil; no key found
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 200,
|
|
||||||
req: req,
|
|
||||||
resp: GetExternalAccountKeysResponse{
|
|
||||||
EAKs: []*linkedca.EABKey{},
|
|
||||||
},
|
|
||||||
db: db,
|
|
||||||
err: nil,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/reference-found": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
chiCtx.URLParams.Add("reference", "an-external-key-reference")
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil)
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
createdAt := time.Now().Add(-24 * time.Hour)
|
|
||||||
var boundAt time.Time
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "an-external-key-reference", reference)
|
|
||||||
return &acme.ExternalAccountKey{
|
|
||||||
ID: "eakID",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 200,
|
|
||||||
req: req,
|
|
||||||
resp: GetExternalAccountKeysResponse{
|
|
||||||
EAKs: []*linkedca.EABKey{
|
|
||||||
{
|
|
||||||
Id: "eakID",
|
|
||||||
Provisioner: "provName",
|
|
||||||
Reference: "an-external-key-reference",
|
|
||||||
CreatedAt: timestamppb.New(createdAt),
|
|
||||||
BoundAt: timestamppb.New(boundAt),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
db: db,
|
|
||||||
err: nil,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/multiple-keys": func(t *testing.T) test {
|
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil)
|
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
createdAt := time.Now().Add(-24 * time.Hour)
|
|
||||||
var boundAt time.Time
|
|
||||||
boundAtSet := time.Now().Add(-12 * time.Hour)
|
|
||||||
db := &acme.MockDB{
|
|
||||||
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
|
||||||
assert.Equals(t, "provID", provisionerID)
|
|
||||||
assert.Equals(t, "", cursor)
|
|
||||||
assert.Equals(t, 0, limit)
|
|
||||||
return []*acme.ExternalAccountKey{
|
|
||||||
{
|
|
||||||
ID: "eakID1",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "some-external-key-reference",
|
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "eakID2",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "some-other-external-key-reference",
|
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
|
||||||
CreatedAt: createdAt.Add(1 * time.Hour),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "eakID3",
|
|
||||||
ProvisionerID: "provID",
|
|
||||||
Reference: "another-external-key-reference",
|
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
BoundAt: boundAtSet,
|
|
||||||
AccountID: "accountID",
|
|
||||||
},
|
|
||||||
}, "", nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
ctx: ctx,
|
|
||||||
statusCode: 200,
|
|
||||||
req: req,
|
|
||||||
resp: GetExternalAccountKeysResponse{
|
|
||||||
EAKs: []*linkedca.EABKey{
|
|
||||||
{
|
|
||||||
Id: "eakID1",
|
|
||||||
Provisioner: "provName",
|
|
||||||
Reference: "some-external-key-reference",
|
|
||||||
CreatedAt: timestamppb.New(createdAt),
|
|
||||||
BoundAt: timestamppb.New(boundAt),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "eakID2",
|
|
||||||
Provisioner: "provName",
|
|
||||||
Reference: "some-other-external-key-reference",
|
|
||||||
CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)),
|
|
||||||
BoundAt: timestamppb.New(boundAt),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "eakID3",
|
|
||||||
Provisioner: "provName",
|
|
||||||
Reference: "another-external-key-reference",
|
|
||||||
CreatedAt: timestamppb.New(createdAt),
|
|
||||||
BoundAt: timestamppb.New(boundAtSet),
|
|
||||||
Account: "accountID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
db: db,
|
|
||||||
err: nil,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for name, prep := range tests {
|
for name, prep := range tests {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
|
||||||
acmeDB: tc.db,
|
|
||||||
}
|
|
||||||
req := tc.req.WithContext(tc.ctx)
|
req := tc.req.WithContext(tc.ctx)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.GetExternalAccountKeys(w, req)
|
acmeResponder := NewACMEAdminResponder()
|
||||||
|
acmeResponder.GetExternalAccountKeys(w, req)
|
||||||
|
|
||||||
res := w.Result()
|
res := w.Result()
|
||||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||||
|
|
||||||
if res.StatusCode >= 400 {
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -1201,22 +582,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
||||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
assert.FatalError(t, err)
|
|
||||||
|
|
||||||
response := GetExternalAccountKeysResponse{}
|
|
||||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
|
||||||
|
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
|
||||||
|
|
||||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})}
|
|
||||||
if !cmp.Equal(tc.resp, response, opts...) {
|
|
||||||
t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,19 @@ import (
|
||||||
|
|
||||||
// Handler is the Admin API request handler.
|
// Handler is the Admin API request handler.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
db admin.DB
|
adminDB admin.DB
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
acmeDB acme.DB
|
acmeDB acme.DB
|
||||||
|
acmeResponder acmeAdminResponderInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler returns a new Authority Config Handler.
|
// NewHandler returns a new Authority Config Handler.
|
||||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler {
|
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
db: adminDB,
|
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
adminDB: adminDB,
|
||||||
acmeDB: acmeDB,
|
acmeDB: acmeDB,
|
||||||
|
acmeResponder: acmeResponder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +49,8 @@ func (h *Handler) Route(r api.Router) {
|
||||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
||||||
|
|
||||||
// ACME External Account Binding Keys
|
// ACME External Account Binding Keys
|
||||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
|
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
|
||||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
|
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
|
||||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.CreateExternalAccountKey)))
|
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey)))
|
||||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey)))
|
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prov, err := h.db.GetProvisioner(ctx, p.GetID())
|
prov, err := h.adminDB.GetProvisioner(ctx, p.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.WriteError(w, err)
|
api.WriteError(w, err)
|
||||||
return
|
return
|
||||||
|
@ -134,7 +134,7 @@ func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
old, err := h.db.GetProvisioner(r.Context(), _old.GetID())
|
old, err := h.adminDB.GetProvisioner(r.Context(), _old.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", _old.GetID()))
|
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", _old.GetID()))
|
||||||
return
|
return
|
||||||
|
|
|
@ -26,7 +26,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
db admin.DB
|
adminDB admin.DB
|
||||||
req *http.Request
|
req *http.Request
|
||||||
statusCode int
|
statusCode int
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
|
@ -104,7 +104,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
req: req,
|
req: req,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -143,7 +143,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
req: req,
|
req: req,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
err: nil,
|
err: nil,
|
||||||
prov: prov,
|
prov: prov,
|
||||||
|
@ -155,7 +155,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
auth: tc.auth,
|
auth: tc.auth,
|
||||||
db: tc.db,
|
adminDB: tc.adminDB,
|
||||||
}
|
}
|
||||||
req := tc.req.WithContext(tc.ctx)
|
req := tc.req.WithContext(tc.ctx)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -605,7 +605,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
body []byte
|
body []byte
|
||||||
db admin.DB
|
adminDB admin.DB
|
||||||
statusCode int
|
statusCode int
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
prov *linkedca.Provisioner
|
prov *linkedca.Provisioner
|
||||||
|
@ -685,7 +685,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -728,7 +728,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -772,7 +772,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -818,7 +818,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -867,7 +867,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -919,7 +919,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: admin.ErrorServerInternalType.String(),
|
Type: admin.ErrorServerInternalType.String(),
|
||||||
|
@ -978,7 +978,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: &admin.Error{
|
err: &admin.Error{
|
||||||
Type: "", // TODO(hs): this error can be improved
|
Type: "", // TODO(hs): this error can be improved
|
||||||
|
@ -1043,7 +1043,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
body: body,
|
body: body,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
db: db,
|
adminDB: db,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
prov: prov,
|
prov: prov,
|
||||||
}
|
}
|
||||||
|
@ -1054,7 +1054,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
auth: tc.auth,
|
auth: tc.auth,
|
||||||
db: tc.db,
|
adminDB: tc.adminDB,
|
||||||
}
|
}
|
||||||
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||||
req = req.WithContext(tc.ctx)
|
req = req.WithContext(tc.ctx)
|
||||||
|
|
|
@ -50,6 +50,7 @@ type Authority struct {
|
||||||
rootX509CertPool *x509.CertPool
|
rootX509CertPool *x509.CertPool
|
||||||
federatedX509Certs []*x509.Certificate
|
federatedX509Certs []*x509.Certificate
|
||||||
certificates *sync.Map
|
certificates *sync.Map
|
||||||
|
x509Enforcers []provisioner.CertificateEnforcer
|
||||||
|
|
||||||
// SCEP CA
|
// SCEP CA
|
||||||
scepService *scep.Service
|
scepService *scep.Service
|
||||||
|
|
|
@ -270,28 +270,36 @@ func (c *Config) GetAudiences() provisioner.Audiences {
|
||||||
|
|
||||||
for _, name := range c.DNSNames {
|
for _, name := range c.DNSNames {
|
||||||
audiences.Sign = append(audiences.Sign,
|
audiences.Sign = append(audiences.Sign,
|
||||||
fmt.Sprintf("https://%s/1.0/sign", name),
|
fmt.Sprintf("https://%s/1.0/sign", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/sign", name),
|
fmt.Sprintf("https://%s/sign", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/1.0/ssh/sign", name),
|
fmt.Sprintf("https://%s/1.0/ssh/sign", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/ssh/sign", name))
|
fmt.Sprintf("https://%s/ssh/sign", toHostname(name)))
|
||||||
audiences.Revoke = append(audiences.Revoke,
|
audiences.Revoke = append(audiences.Revoke,
|
||||||
fmt.Sprintf("https://%s/1.0/revoke", name),
|
fmt.Sprintf("https://%s/1.0/revoke", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/revoke", name))
|
fmt.Sprintf("https://%s/revoke", toHostname(name)))
|
||||||
audiences.SSHSign = append(audiences.SSHSign,
|
audiences.SSHSign = append(audiences.SSHSign,
|
||||||
fmt.Sprintf("https://%s/1.0/ssh/sign", name),
|
fmt.Sprintf("https://%s/1.0/ssh/sign", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/ssh/sign", name),
|
fmt.Sprintf("https://%s/ssh/sign", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/1.0/sign", name),
|
fmt.Sprintf("https://%s/1.0/sign", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/sign", name))
|
fmt.Sprintf("https://%s/sign", toHostname(name)))
|
||||||
audiences.SSHRevoke = append(audiences.SSHRevoke,
|
audiences.SSHRevoke = append(audiences.SSHRevoke,
|
||||||
fmt.Sprintf("https://%s/1.0/ssh/revoke", name),
|
fmt.Sprintf("https://%s/1.0/ssh/revoke", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/ssh/revoke", name))
|
fmt.Sprintf("https://%s/ssh/revoke", toHostname(name)))
|
||||||
audiences.SSHRenew = append(audiences.SSHRenew,
|
audiences.SSHRenew = append(audiences.SSHRenew,
|
||||||
fmt.Sprintf("https://%s/1.0/ssh/renew", name),
|
fmt.Sprintf("https://%s/1.0/ssh/renew", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/ssh/renew", name))
|
fmt.Sprintf("https://%s/ssh/renew", toHostname(name)))
|
||||||
audiences.SSHRekey = append(audiences.SSHRekey,
|
audiences.SSHRekey = append(audiences.SSHRekey,
|
||||||
fmt.Sprintf("https://%s/1.0/ssh/rekey", name),
|
fmt.Sprintf("https://%s/1.0/ssh/rekey", toHostname(name)),
|
||||||
fmt.Sprintf("https://%s/ssh/rekey", name))
|
fmt.Sprintf("https://%s/ssh/rekey", toHostname(name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return audiences
|
return audiences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toHostname(name string) string {
|
||||||
|
// ensure an IPv6 address is represented with square brackets when used as hostname
|
||||||
|
if ip := net.ParseIP(name); ip != nil && ip.To4() == nil {
|
||||||
|
name = "[" + name + "]"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
|
@ -7,9 +7,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/crypto/jose"
|
|
||||||
|
|
||||||
_ "github.com/smallstep/certificates/cas"
|
_ "github.com/smallstep/certificates/cas"
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigValidate(t *testing.T) {
|
func TestConfigValidate(t *testing.T) {
|
||||||
|
@ -298,3 +297,23 @@ func TestAuthConfigValidate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_toHostname(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "localhost", want: "localhost"},
|
||||||
|
{name: "ca.smallstep.com", want: "ca.smallstep.com"},
|
||||||
|
{name: "127.0.0.1", want: "127.0.0.1"},
|
||||||
|
{name: "::1", want: "[::1]"},
|
||||||
|
{name: "[::1]", want: "[::1]"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := toHostname(tt.name); got != tt.want {
|
||||||
|
t.Errorf("toHostname() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -241,6 +241,15 @@ func WithLinkedCAToken(token string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithX509Enforcers is an option that allows to define custom certificate
|
||||||
|
// modifiers that will be processed just before the signing of the certificate.
|
||||||
|
func WithX509Enforcers(ces ...provisioner.CertificateEnforcer) Option {
|
||||||
|
return func(a *Authority) error {
|
||||||
|
a.x509Enforcers = ces
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
|
func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
|
||||||
var block *pem.Block
|
var block *pem.Block
|
||||||
var certs []*x509.Certificate
|
var certs []*x509.Certificate
|
||||||
|
|
|
@ -157,8 +157,9 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
data.SetToken(v)
|
data.SetToken(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Nebula certificate will be available using the template variable Crt.
|
// The Nebula certificate will be available using the template variable
|
||||||
// For example {{ .Crt.Details.Groups }} can be used to get all the groups.
|
// AuthorizationCrt. For example {{ .AuthorizationCrt.Details.Groups }} can
|
||||||
|
// be used to get all the groups.
|
||||||
data.SetAuthorizationCertificate(crt)
|
data.SetAuthorizationCertificate(crt)
|
||||||
|
|
||||||
templateOptions, err := TemplateOptions(p.Options, data)
|
templateOptions, err := TemplateOptions(p.Options, data)
|
||||||
|
|
|
@ -231,6 +231,11 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
data.SetToken(v)
|
data.SetToken(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The X509 certificate will be available using the template variable
|
||||||
|
// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be
|
||||||
|
// used to get all the domains.
|
||||||
|
data.SetAuthorizationCertificate(claims.chains[0][0])
|
||||||
|
|
||||||
templateOptions, err := TemplateOptions(p.Options, data)
|
templateOptions, err := TemplateOptions(p.Options, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign")
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign")
|
||||||
|
@ -306,6 +311,11 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
data.SetToken(v)
|
data.SetToken(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The X509 certificate will be available using the template variable
|
||||||
|
// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be
|
||||||
|
// used to get all the domains.
|
||||||
|
data.SetAuthorizationCertificate(claims.chains[0][0])
|
||||||
|
|
||||||
templateOptions, err := TemplateSSHOptions(p.Options, data)
|
templateOptions, err := TemplateSSHOptions(p.Options, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSSHSign")
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSSHSign")
|
||||||
|
|
|
@ -57,7 +57,7 @@ func (m sshTestCertModifier) Modify(cert *ssh.Certificate, opts provisioner.Sign
|
||||||
if m == "" {
|
if m == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf(string(m))
|
return errors.New(string(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshTestCertValidator string
|
type sshTestCertValidator string
|
||||||
|
@ -66,7 +66,7 @@ func (v sshTestCertValidator) Valid(crt *ssh.Certificate, opts provisioner.SignS
|
||||||
if v == "" {
|
if v == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf(string(v))
|
return errors.New(string(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshTestOptionsValidator string
|
type sshTestOptionsValidator string
|
||||||
|
@ -75,7 +75,7 @@ func (v sshTestOptionsValidator) Valid(opts provisioner.SignSSHOptions) error {
|
||||||
if v == "" {
|
if v == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf(string(v))
|
return errors.New(string(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshTestOptionsModifier string
|
type sshTestOptionsModifier string
|
||||||
|
@ -84,7 +84,7 @@ func (m sshTestOptionsModifier) Modify(cert *ssh.Certificate, opts provisioner.S
|
||||||
if m == "" {
|
if m == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf(string(m))
|
return errors.New(string(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthority_initHostOnly(t *testing.T) {
|
func TestAuthority_initHostOnly(t *testing.T) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -180,6 +181,17 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process injected modifiers after validation
|
||||||
|
for _, m := range a.x509Enforcers {
|
||||||
|
if err := m.Enforce(leaf); err != nil {
|
||||||
|
return nil, errs.ApplyOptions(
|
||||||
|
errs.ForbiddenErr(err, "error creating certificate"),
|
||||||
|
opts...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign certificate
|
||||||
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
||||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||||
Template: leaf,
|
Template: leaf,
|
||||||
|
@ -508,8 +520,19 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
||||||
return fatal(errors.New("private key is not a crypto.Signer"))
|
return fatal(errors.New("private key is not a crypto.Signer"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepare the sans: IPv6 DNS hostname representations are converted to their IP representation
|
||||||
|
sans := make([]string, len(a.config.DNSNames))
|
||||||
|
for i, san := range a.config.DNSNames {
|
||||||
|
if strings.HasPrefix(san, "[") && strings.HasSuffix(san, "]") {
|
||||||
|
if ip := net.ParseIP(san[1 : len(san)-1]); ip != nil {
|
||||||
|
san = ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sans[i] = san
|
||||||
|
}
|
||||||
|
|
||||||
// Create initial certificate request.
|
// Create initial certificate request.
|
||||||
cr, err := x509util.CreateCertificateRequest("Step Online CA", a.config.DNSNames, signer)
|
cr, err := x509util.CreateCertificateRequest("Step Online CA", sans, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal(err)
|
return fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,6 +205,17 @@ type basicConstraints struct {
|
||||||
MaxPathLen int `asn1:"optional,default:-1"`
|
MaxPathLen int `asn1:"optional,default:-1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testEnforcer struct {
|
||||||
|
enforcer func(*x509.Certificate) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *testEnforcer) Enforce(cert *x509.Certificate) error {
|
||||||
|
if e.enforcer != nil {
|
||||||
|
return e.enforcer(cert)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthority_Sign(t *testing.T) {
|
func TestAuthority_Sign(t *testing.T) {
|
||||||
pub, priv, err := keyutil.GenerateDefaultKeyPair()
|
pub, priv, err := keyutil.GenerateDefaultKeyPair()
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -244,6 +255,7 @@ func TestAuthority_Sign(t *testing.T) {
|
||||||
extraOpts []provisioner.SignOption
|
extraOpts []provisioner.SignOption
|
||||||
notBefore time.Time
|
notBefore time.Time
|
||||||
notAfter time.Time
|
notAfter time.Time
|
||||||
|
extensionsCount int
|
||||||
err error
|
err error
|
||||||
code int
|
code int
|
||||||
}
|
}
|
||||||
|
@ -454,6 +466,49 @@ ZYtQ9Ot36qc=
|
||||||
code: http.StatusInternalServerError,
|
code: http.StatusInternalServerError,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fail with provisioner enforcer": func(t *testing.T) *signTest {
|
||||||
|
csr := getCSR(t, priv)
|
||||||
|
aa := testAuthority(t)
|
||||||
|
aa.db = &db.MockAuthDB{
|
||||||
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
||||||
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &signTest{
|
||||||
|
auth: aa,
|
||||||
|
csr: csr,
|
||||||
|
extraOpts: append(extraOpts, &testEnforcer{
|
||||||
|
enforcer: func(crt *x509.Certificate) error { return fmt.Errorf("an error") },
|
||||||
|
}),
|
||||||
|
signOpts: signOpts,
|
||||||
|
err: errors.New("error creating certificate"),
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail with custom enforcer": func(t *testing.T) *signTest {
|
||||||
|
csr := getCSR(t, priv)
|
||||||
|
aa := testAuthority(t, WithX509Enforcers(&testEnforcer{
|
||||||
|
enforcer: func(cert *x509.Certificate) error {
|
||||||
|
return fmt.Errorf("an error")
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
aa.db = &db.MockAuthDB{
|
||||||
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
||||||
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &signTest{
|
||||||
|
auth: aa,
|
||||||
|
csr: csr,
|
||||||
|
extraOpts: extraOpts,
|
||||||
|
signOpts: signOpts,
|
||||||
|
err: errors.New("error creating certificate"),
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
},
|
||||||
"ok": func(t *testing.T) *signTest {
|
"ok": func(t *testing.T) *signTest {
|
||||||
csr := getCSR(t, priv)
|
csr := getCSR(t, priv)
|
||||||
_a := testAuthority(t)
|
_a := testAuthority(t)
|
||||||
|
@ -470,6 +525,7 @@ ZYtQ9Ot36qc=
|
||||||
signOpts: signOpts,
|
signOpts: signOpts,
|
||||||
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
||||||
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
||||||
|
extensionsCount: 6,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ok with enforced modifier": func(t *testing.T) *signTest {
|
"ok with enforced modifier": func(t *testing.T) *signTest {
|
||||||
|
@ -503,6 +559,7 @@ ZYtQ9Ot36qc=
|
||||||
signOpts: signOpts,
|
signOpts: signOpts,
|
||||||
notBefore: now.Truncate(time.Second),
|
notBefore: now.Truncate(time.Second),
|
||||||
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
|
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
|
||||||
|
extensionsCount: 6,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ok with custom template": func(t *testing.T) *signTest {
|
"ok with custom template": func(t *testing.T) *signTest {
|
||||||
|
@ -536,6 +593,7 @@ ZYtQ9Ot36qc=
|
||||||
signOpts: signOpts,
|
signOpts: signOpts,
|
||||||
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
||||||
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
||||||
|
extensionsCount: 6,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ok/csr with no template critical SAN extension": func(t *testing.T) *signTest {
|
"ok/csr with no template critical SAN extension": func(t *testing.T) *signTest {
|
||||||
|
@ -564,6 +622,33 @@ ZYtQ9Ot36qc=
|
||||||
signOpts: provisioner.SignOptions{},
|
signOpts: provisioner.SignOptions{},
|
||||||
notBefore: now.Truncate(time.Second),
|
notBefore: now.Truncate(time.Second),
|
||||||
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
|
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
|
||||||
|
extensionsCount: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok with custom enforcer": func(t *testing.T) *signTest {
|
||||||
|
csr := getCSR(t, priv)
|
||||||
|
aa := testAuthority(t, WithX509Enforcers(&testEnforcer{
|
||||||
|
enforcer: func(cert *x509.Certificate) error {
|
||||||
|
cert.CRLDistributionPoints = []string{"http://ca.example.org/leaf.crl"}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
||||||
|
aa.db = &db.MockAuthDB{
|
||||||
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
||||||
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||||
|
assert.Equals(t, crt.CRLDistributionPoints, []string{"http://ca.example.org/leaf.crl"})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &signTest{
|
||||||
|
auth: aa,
|
||||||
|
csr: csr,
|
||||||
|
extraOpts: extraOpts,
|
||||||
|
signOpts: signOpts,
|
||||||
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
||||||
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
||||||
|
extensionsCount: 7,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -645,9 +730,6 @@ ZYtQ9Ot36qc=
|
||||||
// Empty CSR subject test does not use any provisioner extensions.
|
// Empty CSR subject test does not use any provisioner extensions.
|
||||||
// So provisioner ID ext will be missing.
|
// So provisioner ID ext will be missing.
|
||||||
found = 1
|
found = 1
|
||||||
assert.Len(t, 5, leaf.Extensions)
|
|
||||||
} else {
|
|
||||||
assert.Len(t, 6, leaf.Extensions)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -655,6 +737,7 @@ ZYtQ9Ot36qc=
|
||||||
realIntermediate, err := x509.ParseCertificate(issuer.Raw)
|
realIntermediate, err := x509.ParseCertificate(issuer.Raw)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
assert.Equals(t, intermediate, realIntermediate)
|
assert.Equals(t, intermediate, realIntermediate)
|
||||||
|
assert.Len(t, tc.extensionsCount, leaf.Extensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
|
||||||
adminAPI "github.com/smallstep/certificates/authority/admin/api"
|
adminAPI "github.com/smallstep/certificates/authority/admin/api"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
@ -40,6 +39,19 @@ type AdminClient struct {
|
||||||
x5cSubject string
|
x5cSubject string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminClientError is the client side representation of an
|
||||||
|
// AdminError returned by the CA.
|
||||||
|
type AdminClientError struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the AdminClientError message as the error message
|
||||||
|
func (e *AdminClientError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
// NewAdminClient creates a new AdminClient with the given endpoint and options.
|
// NewAdminClient creates a new AdminClient with the given endpoint and options.
|
||||||
func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) {
|
func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) {
|
||||||
u, err := parseEndpoint(endpoint)
|
u, err := parseEndpoint(endpoint)
|
||||||
|
@ -670,9 +682,9 @@ retry:
|
||||||
func readAdminError(r io.ReadCloser) error {
|
func readAdminError(r io.ReadCloser) error {
|
||||||
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
adminErr := new(admin.Error)
|
adminErr := new(AdminClientError)
|
||||||
if err := json.NewDecoder(r).Decode(adminErr); err != nil {
|
if err := json.NewDecoder(r).Decode(adminErr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return errors.New(adminErr.Message)
|
return adminErr
|
||||||
}
|
}
|
||||||
|
|
3
ca/ca.go
3
ca/ca.go
|
@ -207,7 +207,8 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
||||||
if cfg.AuthorityConfig.EnableAdmin {
|
if cfg.AuthorityConfig.EnableAdmin {
|
||||||
adminDB := auth.GetAdminDatabase()
|
adminDB := auth.GetAdminDatabase()
|
||||||
if adminDB != nil {
|
if adminDB != nil {
|
||||||
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB)
|
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
|
||||||
|
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder)
|
||||||
mux.Route("/admin", func(r chi.Router) {
|
mux.Route("/admin", func(r chi.Router) {
|
||||||
adminHandler.Route(r)
|
adminHandler.Route(r)
|
||||||
})
|
})
|
||||||
|
|
|
@ -155,11 +155,11 @@ func (p *Provisioner) SSHToken(certType, keyID string, principals []string) (str
|
||||||
func decryptProvisionerJWK(encryptedKey string, password []byte) (*jose.JSONWebKey, error) {
|
func decryptProvisionerJWK(encryptedKey string, password []byte) (*jose.JSONWebKey, error) {
|
||||||
enc, err := jose.ParseEncrypted(encryptedKey)
|
enc, err := jose.ParseEncrypted(encryptedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "error parsing provisioner encrypted key")
|
||||||
}
|
}
|
||||||
data, err := enc.Decrypt(password)
|
data, err := enc.Decrypt(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "error decrypting provisioner key with provided password")
|
||||||
}
|
}
|
||||||
jwk := new(jose.JSONWebKey)
|
jwk := new(jose.JSONWebKey)
|
||||||
if err := json.Unmarshal(data, jwk); err != nil {
|
if err := json.Unmarshal(data, jwk); err != nil {
|
||||||
|
|
|
@ -200,6 +200,102 @@ func TestProvisioner_Token(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_IPv6Token(t *testing.T) {
|
||||||
|
p := getTestProvisioner(t, "https://[::1]:9000")
|
||||||
|
sha := "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7"
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
name string
|
||||||
|
kid string
|
||||||
|
fingerprint string
|
||||||
|
jwk *jose.JSONWebKey
|
||||||
|
tokenLifetime time.Duration
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
subject string
|
||||||
|
sans []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"subject", nil}, false},
|
||||||
|
{"ok-with-san", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"subject", []string{"foo.smallstep.com"}}, false},
|
||||||
|
{"ok-with-sans", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"subject", []string{"foo.smallstep.com", "127.0.0.1"}}, false},
|
||||||
|
{"fail-no-subject", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"", []string{"foo.smallstep.com"}}, true},
|
||||||
|
{"fail-no-key", fields{p.name, p.kid, sha, &jose.JSONWebKey{}, p.tokenLifetime}, args{"subject", nil}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := &Provisioner{
|
||||||
|
name: tt.fields.name,
|
||||||
|
kid: tt.fields.kid,
|
||||||
|
audience: "https://[::1]:9000/1.0/sign",
|
||||||
|
fingerprint: tt.fields.fingerprint,
|
||||||
|
jwk: tt.fields.jwk,
|
||||||
|
tokenLifetime: tt.fields.tokenLifetime,
|
||||||
|
}
|
||||||
|
got, err := p.Token(tt.args.subject, tt.args.sans...)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Provisioner.Token() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr == false {
|
||||||
|
jwt, err := jose.ParseSigned(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var claims jose.Claims
|
||||||
|
if err := jwt.Claims(tt.fields.jwk.Public(), &claims); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := claims.ValidateWithLeeway(jose.Expected{
|
||||||
|
Audience: []string{"https://[::1]:9000/1.0/sign"},
|
||||||
|
Issuer: tt.fields.name,
|
||||||
|
Subject: tt.args.subject,
|
||||||
|
Time: time.Now().UTC(),
|
||||||
|
}, time.Minute); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lifetime := claims.Expiry.Time().Sub(claims.NotBefore.Time())
|
||||||
|
if lifetime != tt.fields.tokenLifetime {
|
||||||
|
t.Errorf("Claims token life time = %s, want %s", lifetime, tt.fields.tokenLifetime)
|
||||||
|
}
|
||||||
|
allClaims := make(map[string]interface{})
|
||||||
|
if err := jwt.Claims(tt.fields.jwk.Public(), &allClaims); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v, ok := allClaims["sha"].(string); !ok || v != sha {
|
||||||
|
t.Errorf("Claim sha = %s, want %s", v, sha)
|
||||||
|
}
|
||||||
|
if len(tt.args.sans) == 0 {
|
||||||
|
if v, ok := allClaims["sans"].([]interface{}); !ok || !reflect.DeepEqual(v, []interface{}{tt.args.subject}) {
|
||||||
|
t.Errorf("Claim sans = %s, want %s", v, []interface{}{tt.args.subject})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
want := []interface{}{}
|
||||||
|
for _, s := range tt.args.sans {
|
||||||
|
want = append(want, s)
|
||||||
|
}
|
||||||
|
if v, ok := allClaims["sans"].([]interface{}); !ok || !reflect.DeepEqual(v, want) {
|
||||||
|
t.Errorf("Claim sans = %s, want %s", v, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := allClaims["jti"].(string); !ok || v == "" {
|
||||||
|
t.Errorf("Claim jti = %s, want not blank", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProvisioner_SSHToken(t *testing.T) {
|
func TestProvisioner_SSHToken(t *testing.T) {
|
||||||
p := getTestProvisioner(t, "https://127.0.0.1:9000")
|
p := getTestProvisioner(t, "https://127.0.0.1:9000")
|
||||||
sha := "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7"
|
sha := "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7"
|
||||||
|
|
|
@ -75,10 +75,11 @@ This tool is experimental and in the future it will be integrated in step cli.
|
||||||
OPTIONS`)
|
OPTIONS`)
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Fprintln(os.Stderr, `
|
fmt.Fprintf(os.Stderr, `
|
||||||
COPYRIGHT
|
COPYRIGHT
|
||||||
|
|
||||||
(c) 2018-2020 Smallstep Labs, Inc.`)
|
(c) 2018-%d Smallstep Labs, Inc.
|
||||||
|
`, time.Now().Year())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt
|
||||||
'''`
|
'''`
|
||||||
app.Flags = append(app.Flags, commands.AppCommand.Flags...)
|
app.Flags = append(app.Flags, commands.AppCommand.Flags...)
|
||||||
app.Flags = append(app.Flags, cli.HelpFlag)
|
app.Flags = append(app.Flags, cli.HelpFlag)
|
||||||
app.Copyright = "(c) 2018-2020 Smallstep Labs, Inc."
|
app.Copyright = fmt.Sprintf("(c) 2018-%d Smallstep Labs, Inc.", time.Now().Year())
|
||||||
|
|
||||||
// All non-successful output should be written to stderr
|
// All non-successful output should be written to stderr
|
||||||
app.Writer = os.Stdout
|
app.Writer = os.Stdout
|
||||||
|
|
|
@ -105,10 +105,11 @@ This tool is experimental and in the future it will be integrated in step cli.
|
||||||
OPTIONS`)
|
OPTIONS`)
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Fprintln(os.Stderr, `
|
fmt.Fprintf(os.Stderr, `
|
||||||
COPYRIGHT
|
COPYRIGHT
|
||||||
|
|
||||||
(c) 2018-2020 Smallstep Labs, Inc.`)
|
(c) 2018-%d Smallstep Labs, Inc.
|
||||||
|
`, time.Now().Year())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,10 +250,11 @@ This tool is experimental and in the future it will be integrated in step cli.
|
||||||
OPTIONS`)
|
OPTIONS`)
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Fprintln(os.Stderr, `
|
fmt.Fprintf(os.Stderr, `
|
||||||
COPYRIGHT
|
COPYRIGHT
|
||||||
|
|
||||||
(c) 2018-2021 Smallstep Labs, Inc.`)
|
(c) 2018-%d Smallstep Labs, Inc.
|
||||||
|
`, time.Now().Year())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,10 +148,11 @@ This tool is experimental and in the future it will be integrated in step cli.
|
||||||
OPTIONS`)
|
OPTIONS`)
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Fprintln(os.Stderr, `
|
fmt.Fprintf(os.Stderr, `
|
||||||
COPYRIGHT
|
COPYRIGHT
|
||||||
|
|
||||||
(c) 2018-2020 Smallstep Labs, Inc.`)
|
(c) 2018-%d Smallstep Labs, Inc.
|
||||||
|
`, time.Now().Year())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,7 @@ func (e *Error) UnmarshalJSON(data []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.Status = er.Status
|
e.Status = er.Status
|
||||||
e.Err = fmt.Errorf(er.Message)
|
e.Err = fmt.Errorf("%s", er.Message)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ func TestError_UnmarshalJSON(t *testing.T) {
|
||||||
t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(tt.expected, e) {
|
if !reflect.DeepEqual(tt.expected, e) {
|
||||||
t.Errorf("Error.UnmarshalJSON() wants = %v, got %v", tt.expected, e)
|
t.Errorf("Error.UnmarshalJSON() wants = %+v, got %+v", tt.expected, e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
18
examples/ansible/smallstep-certs/defaults/main.yml
Normal file
18
examples/ansible/smallstep-certs/defaults/main.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Root cert for each will be saved in /etc/ssl/smallstep/ca/{{ ca_name }}/certs/root_ca.crt
|
||||||
|
smallstep_root_certs: []
|
||||||
|
# -
|
||||||
|
# ca_name: your_ca
|
||||||
|
# ca_url: "https://certs.your_ca.ca.smallstep.com"
|
||||||
|
# ca_fingerprint: "56092...2200"
|
||||||
|
|
||||||
|
# Each leaf cert will be saved in /etc/ssl/smallstep/leaf/{{ cert_subject }}/{{ cert_subject }}.crt|key
|
||||||
|
smallstep_leaf_certs: []
|
||||||
|
# -
|
||||||
|
# ca_name: your_ca
|
||||||
|
# cert_subject: "{{ inventory_hostname }}"
|
||||||
|
# provisioner_name: "admin"
|
||||||
|
# provisioner_password: "{{ smallstep_ssh_provisioner_password }}"
|
||||||
|
|
44
examples/ansible/smallstep-certs/tasks/main.yml
Normal file
44
examples/ansible/smallstep-certs/tasks/main.yml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
- name: "Ensure provisioners directories exist"
|
||||||
|
file:
|
||||||
|
path: "/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}"
|
||||||
|
state: directory
|
||||||
|
mode: 0600
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
with_items: "{{ smallstep_leaf_certs }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: "Ensure provisioner passwords are up to date"
|
||||||
|
copy:
|
||||||
|
dest: "/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt"
|
||||||
|
content: "{{ item.provisioner_password }}"
|
||||||
|
mode: 0700
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
with_items: "{{ smallstep_leaf_certs }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: "Get root certs for CAs"
|
||||||
|
command:
|
||||||
|
cmd: "step ca bootstrap --context {{ item.context }} --ca-url {{ item.ca_url }} --fingerprint {{ item.ca_fingerprint }}"
|
||||||
|
with_items: "{{ smallstep_root_certs }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: "Get leaf certs"
|
||||||
|
command:
|
||||||
|
cmd: "step ca certificate --context {{ item.context }} {{ item.cert_subject }} {{ item.cert_path }} {{ item.key_path }} --force --console --provisioner {{ item.provisioner_name }} --provisioner-password-file /etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt"
|
||||||
|
with_items: "{{ smallstep_leaf_certs }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Ensure cron to renew leaf certs is up to date
|
||||||
|
cron:
|
||||||
|
user: "root"
|
||||||
|
name: "renew leaf cert {{ item.cert_subject }}"
|
||||||
|
cron_file: smallstep
|
||||||
|
job: "step ca renew --context {{ item.context }} {{ item.cert_path }} {{ item.key_path }} --expires-in 6h --force >> /var/log/smallstep-{{ item.cert_subject }}.log 2>&1"
|
||||||
|
state: present
|
||||||
|
minute: "*/30"
|
||||||
|
with_items: "{{ smallstep_leaf_certs }}"
|
||||||
|
when: "{{ item.cron_renew }}"
|
||||||
|
no_log: true
|
2
examples/ansible/smallstep-install/defaults/main.yml
Normal file
2
examples/ansible/smallstep-install/defaults/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
smallstep_install_step_version: 0.15.3
|
||||||
|
smallstep_install_step_ssh_version: 0.19.1-1
|
29
examples/ansible/smallstep-install/tasks/main.yml
Normal file
29
examples/ansible/smallstep-install/tasks/main.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
# These steps automate the installation guide here:
|
||||||
|
# https://smallstep.com/docs/sso-ssh/hosts/
|
||||||
|
|
||||||
|
- name: Download step binary
|
||||||
|
get_url:
|
||||||
|
url: "https://files.smallstep.com/step-linux-{{ smallstep_install_step_version }}"
|
||||||
|
dest: "/usr/local/bin/step-{{ smallstep_install_step_version }}"
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Link binaries to correct version
|
||||||
|
file:
|
||||||
|
src: "/usr/local/bin/step-{{ smallstep_install_step_version }}"
|
||||||
|
dest: "{{ item }}"
|
||||||
|
state: link
|
||||||
|
with_items:
|
||||||
|
- /usr/bin/step
|
||||||
|
- /usr/local/bin/step
|
||||||
|
|
||||||
|
- name: Link /usr/local/bin/step to correct binary version
|
||||||
|
file:
|
||||||
|
src: "/usr/local/bin/step-{{ smallstep_install_step_version }}"
|
||||||
|
dest: /usr/local/bin/step
|
||||||
|
state: link
|
||||||
|
|
||||||
|
- name: Ensure step-ssh is installed
|
||||||
|
apt:
|
||||||
|
deb: "https://files.smallstep.com/step-ssh_{{ smallstep_install_step_ssh_version }}_amd64.deb"
|
||||||
|
state: present
|
8
examples/ansible/smallstep-ssh/defaults/main.yml
Normal file
8
examples/ansible/smallstep-ssh/defaults/main.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# If this host is behind a bastion this variable should contain the hostname of the bastion
|
||||||
|
smallstep_ssh_host_behind_bastion_name: ""
|
||||||
|
smallstep_ssh_host_is_bastion: false
|
||||||
|
smallstep_ssh_ca_url: "https://ssh.mycompany.ca.smallstep.com"
|
||||||
|
smallstep_ssh_ca_fingerprint: "XXXXXXXXXXXXXXX"
|
||||||
|
|
||||||
|
# Whether or not to reinitialize the host even if it's already been installed
|
||||||
|
smallstep_ssh_force_reinit: true
|
41
examples/ansible/smallstep-ssh/tasks/main.yml
Normal file
41
examples/ansible/smallstep-ssh/tasks/main.yml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
# These steps automate the installation guide here:
|
||||||
|
# https://smallstep.com/docs/sso-ssh/hosts/
|
||||||
|
|
||||||
|
# TODO: Figure out how to make this idempotent instead of reinstalling on each run
|
||||||
|
|
||||||
|
- name: Bootstrap node to connect to CA
|
||||||
|
command: "step ca bootstrap --context ssh --ca-url {{ smallstep_ssh_ca_url }} --fingerprint {{ smallstep_ssh_ca_fingerprint }} --force"
|
||||||
|
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||||
|
|
||||||
|
- name: Get a host SSH certificate
|
||||||
|
command: "step ssh certificate --context ssh {{ inventory_hostname }} /etc/ssh/ssh_host_ecdsa_key.pub --host --sign --provisioner=\"Service Account\" --token=\"{{ smallstep_ssh_enrollment_token }}\" --force"
|
||||||
|
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||||
|
|
||||||
|
- name: Configure SSHD (will be overwriten by the sshd template in Ansible later)
|
||||||
|
command: "step ssh config --context ssh --host --set Certificate=ssh_host_ecdsa_key-cert.pub --set Key=ssh_host_ecdsa_key"
|
||||||
|
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||||
|
|
||||||
|
- name: Activate SmallStep PAM/NSS modules and nohup sshd
|
||||||
|
command: "step-ssh activate {{ inventory_hostname }}"
|
||||||
|
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||||
|
|
||||||
|
- name: Generate host tags list
|
||||||
|
set_fact:
|
||||||
|
smallstep_ssh_host_tags_string: "{{ smallstep_ssh_host_tags | to_json | regex_replace('\\:\\ ','=') | regex_replace('\\{\\\"|,\\ \\\"', ' --tag \"') | regex_replace('[\\[\\]{}]') }}"
|
||||||
|
|
||||||
|
- name: Generate command to register
|
||||||
|
set_fact:
|
||||||
|
smallstep_ssh_register_string: |
|
||||||
|
step-ssh-ctl register
|
||||||
|
--hostname {{ inventory_hostname }}
|
||||||
|
{% if not smallstep_ssh_host_is_bastion %}--bastion '{{ smallstep_ssh_host_behind_bastion_name|default("") }}'{% endif %}
|
||||||
|
{% if smallstep_ssh_host_is_bastion %}--is-bastion{% endif %}
|
||||||
|
{{ smallstep_ssh_host_tags_string }}
|
||||||
|
|
||||||
|
- debug: var=smallstep_ssh_register_string
|
||||||
|
|
||||||
|
- name: Register host with smallstep
|
||||||
|
command: "{{ smallstep_ssh_register_string }}"
|
||||||
|
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||||
|
|
Loading…
Reference in a new issue