Merge branch 'master' into herman/allow-deny

This commit is contained in:
Herman Slatman 2022-03-24 18:35:20 +01:00
commit 23676d3bcc
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
7 changed files with 163 additions and 170 deletions

View file

@ -13,7 +13,6 @@ import (
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/certificates/logging"
"github.com/smallstep/certificates/scep"
)
// WriteError writes to w a JSON representation of the given error.
@ -25,22 +24,9 @@ func WriteError(w http.ResponseWriter, err error) {
case *admin.Error:
admin.WriteError(w, k)
return
case *scep.Error:
w.Header().Set("Content-Type", "text/plain")
default:
w.Header().Set("Content-Type", "application/json")
}
cause := errors.Cause(err)
if sc, ok := err.(errs.StatusCoder); ok {
w.WriteHeader(sc.StatusCode())
} else {
if sc, ok := cause.(errs.StatusCoder); ok {
w.WriteHeader(sc.StatusCode())
} else {
w.WriteHeader(http.StatusInternalServerError)
}
}
// Write errors in the response writer
if rl, ok := w.(logging.ResponseLogger); ok {
@ -60,6 +46,16 @@ func WriteError(w http.ResponseWriter, err error) {
}
}
code := http.StatusInternalServerError
if sc, ok := err.(errs.StatusCoder); ok {
code = sc.StatusCode()
} else if sc, ok := cause.(errs.StatusCoder); ok {
code = sc.StatusCode()
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(err); err != nil {
log.Error(w, err)
}

View file

@ -117,7 +117,7 @@ func main() {
app.HelpName = "step-ca"
app.Version = step.Version()
app.Usage = "an online certificate authority for secure automated certificate management"
app.UsageText = `**step-ca** <config> [**--password-file**=<file>]
app.UsageText = `**step-ca** [config] [**--context**=<name>] [**--password-file**=<file>]
[**--ssh-host-password-file**=<file>] [**--ssh-user-password-file**=<file>]
[**--issuer-password-file**=<file>] [**--resolver**=<addr>] [**--help**] [**--version**]`
app.Description = `**step-ca** runs the Step Online Certificate Authority
@ -133,6 +133,7 @@ This command will run indefinitely on success and return \>0 if any error occurs
These examples assume that you have already initialized your PKI by running
'step ca init'. If you have not completed this step please see the 'Getting Started'
section of the README.
Run the Step CA and prompt for password:
'''
$ step-ca $STEPPATH/config/ca.json
@ -141,7 +142,26 @@ Run the Step CA and read the password from a file - this is useful for
automating deployment:
'''
$ step-ca $STEPPATH/config/ca.json --password-file ./password.txt
'''`
'''
Run the Step CA for the context selected with step and a custom password file:
'''
$ step context select ssh
$ step-ca --password-file ./password.txt
'''
Run the Step CA for the context named _mybiz_ and prompt for password:
'''
$ step-ca --context=mybiz
'''
Run the Step CA for the context named _mybiz_ and an alternate ca.json file:
'''
$ step-ca --context=mybiz other-ca.json
'''
Run the Step CA for the context named _mybiz_ and read the password from a file - this is useful for
automating deployment:
'''
$ step-ca --context=mybiz --password-file ./password.txt
'''
`
app.Flags = append(app.Flags, commands.AppCommand.Flags...)
app.Flags = append(app.Flags, cli.HelpFlag)
app.Copyright = fmt.Sprintf("(c) 2018-%d Smallstep Labs, Inc.", time.Now().Year())

View file

@ -16,6 +16,7 @@ import (
"github.com/smallstep/certificates/pki"
"github.com/urfave/cli"
"go.step.sm/cli-utils/errs"
"go.step.sm/cli-utils/step"
)
// AppCommand is the action used as the top action.
@ -57,6 +58,11 @@ certificate issuer private key used in the RA mode.`,
Usage: "token used to enable the linked ca.",
EnvVar: "STEP_CA_TOKEN",
},
cli.StringFlag{
Name: "context",
Usage: "The name of the authority's context.",
EnvVar: "STEP_CA_CONTEXT",
},
},
}
@ -69,15 +75,23 @@ func appAction(ctx *cli.Context) error {
resolver := ctx.String("resolver")
token := ctx.String("token")
// If zero cmd line args show help, if >1 cmd line args show error.
if ctx.NArg() == 0 {
return cli.ShowAppHelp(ctx)
}
if err := errs.NumberOfArguments(ctx, 1); err != nil {
return err
if ctx.NArg() > 1 {
return errs.TooManyArguments(ctx)
}
if caCtx := ctx.String("context"); caCtx != "" {
if err := step.Contexts().SetCurrent(caCtx); err != nil {
return err
}
}
var configFile string
if ctx.NArg() > 0 {
configFile = ctx.Args().Get(0)
} else {
configFile = step.CaConfigFile()
}
configFile := ctx.Args().Get(0)
cfg, err := config.LoadConfiguration(configFile)
if err != nil {
fatal(err)

View file

@ -1,9 +1,12 @@
// Package api implements a SCEP HTTP server.
package api
import (
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
@ -11,7 +14,6 @@ import (
"github.com/go-chi/chi"
microscep "github.com/micromdm/scep/v2/scep"
"github.com/pkg/errors"
"go.mozilla.org/pkcs7"
"github.com/smallstep/certificates/api"
@ -30,22 +32,14 @@ const (
const maxPayloadSize = 2 << 20
type nextHTTP = func(http.ResponseWriter, *http.Request)
const (
certChainHeader = "application/x-x509-ca-ra-cert"
leafHeader = "application/x-x509-ca-cert"
pkiOperationHeader = "application/x-pki-message"
)
// SCEPRequest is a SCEP server request.
type SCEPRequest struct {
// request is a SCEP server request.
type request struct {
Operation string
Message []byte
}
// SCEPResponse is a SCEP server response.
type SCEPResponse struct {
// response is a SCEP server response.
type response struct {
Operation string
CACertNum int
Data []byte
@ -53,19 +47,21 @@ type SCEPResponse struct {
Error error
}
// Handler is the SCEP request handler.
type Handler struct {
Auth scep.Interface
// handler is the SCEP request handler.
type handler struct {
auth *scep.Authority
}
// New returns a new SCEP API router.
func New(scepAuth scep.Interface) api.RouterHandler {
return &Handler{scepAuth}
func New(auth *scep.Authority) api.RouterHandler {
return &handler{
auth: auth,
}
}
// Route traffic and implement the Router interface.
func (h *Handler) Route(r api.Router) {
getLink := h.Auth.GetLinkExplicit
func (h *handler) Route(r api.Router) {
getLink := h.auth.GetLinkExplicit
r.MethodFunc(http.MethodGet, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Get))
r.MethodFunc(http.MethodGet, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Get))
r.MethodFunc(http.MethodPost, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Post))
@ -73,64 +69,64 @@ func (h *Handler) Route(r api.Router) {
}
// Get handles all SCEP GET requests
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
func (h *handler) Get(w http.ResponseWriter, r *http.Request) {
request, err := decodeSCEPRequest(r)
req, err := decodeRequest(r)
if err != nil {
writeError(w, errors.Wrap(err, "invalid scep get request"))
fail(w, fmt.Errorf("invalid scep get request: %w", err))
return
}
ctx := r.Context()
var response SCEPResponse
var res response
switch request.Operation {
switch req.Operation {
case opnGetCACert:
response, err = h.GetCACert(ctx)
res, err = h.GetCACert(ctx)
case opnGetCACaps:
response, err = h.GetCACaps(ctx)
res, err = h.GetCACaps(ctx)
case opnPKIOperation:
// TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though
default:
err = errors.Errorf("unknown operation: %s", request.Operation)
err = fmt.Errorf("unknown operation: %s", req.Operation)
}
if err != nil {
writeError(w, errors.Wrap(err, "scep get request failed"))
fail(w, fmt.Errorf("scep get request failed: %w", err))
return
}
writeSCEPResponse(w, response)
writeResponse(w, res)
}
// Post handles all SCEP POST requests
func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
func (h *handler) Post(w http.ResponseWriter, r *http.Request) {
request, err := decodeSCEPRequest(r)
req, err := decodeRequest(r)
if err != nil {
writeError(w, errors.Wrap(err, "invalid scep post request"))
fail(w, fmt.Errorf("invalid scep post request: %w", err))
return
}
ctx := r.Context()
var response SCEPResponse
var res response
switch request.Operation {
switch req.Operation {
case opnPKIOperation:
response, err = h.PKIOperation(ctx, request)
res, err = h.PKIOperation(ctx, req)
default:
err = errors.Errorf("unknown operation: %s", request.Operation)
err = fmt.Errorf("unknown operation: %s", req.Operation)
}
if err != nil {
writeError(w, errors.Wrap(err, "scep post request failed"))
fail(w, fmt.Errorf("scep post request failed: %w", err))
return
}
writeSCEPResponse(w, response)
writeResponse(w, res)
}
func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
func decodeRequest(r *http.Request) (request, error) {
defer r.Body.Close()
@ -146,7 +142,7 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
case http.MethodGet:
switch operation {
case opnGetCACert, opnGetCACaps:
return SCEPRequest{
return request{
Operation: operation,
Message: []byte{},
}, nil
@ -158,50 +154,50 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
// TODO: verify this; it seems like it should be StdEncoding instead of URLEncoding
decodedMessage, err := base64.URLEncoding.DecodeString(message)
if err != nil {
return SCEPRequest{}, err
return request{}, err
}
return SCEPRequest{
return request{
Operation: operation,
Message: decodedMessage,
}, nil
default:
return SCEPRequest{}, errors.Errorf("unsupported operation: %s", operation)
return request{}, fmt.Errorf("unsupported operation: %s", operation)
}
case http.MethodPost:
body, err := io.ReadAll(io.LimitReader(r.Body, maxPayloadSize))
if err != nil {
return SCEPRequest{}, err
return request{}, err
}
return SCEPRequest{
return request{
Operation: operation,
Message: body,
}, nil
default:
return SCEPRequest{}, errors.Errorf("unsupported method: %s", method)
return request{}, fmt.Errorf("unsupported method: %s", method)
}
}
// lookupProvisioner loads the provisioner associated with the request.
// Responds 404 if the provisioner does not exist.
func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "provisionerName")
provisionerName, err := url.PathUnescape(name)
if err != nil {
api.WriteError(w, errors.Errorf("error url unescaping provisioner name '%s'", name))
fail(w, fmt.Errorf("error url unescaping provisioner name '%s'", name))
return
}
p, err := h.Auth.LoadProvisionerByName(provisionerName)
p, err := h.auth.LoadProvisionerByName(provisionerName)
if err != nil {
api.WriteError(w, err)
fail(w, err)
return
}
prov, ok := p.(*provisioner.SCEP)
if !ok {
api.WriteError(w, errors.New("provisioner must be of type SCEP"))
fail(w, errors.New("provisioner must be of type SCEP"))
return
}
@ -212,59 +208,59 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
}
// GetCACert returns the CA certificates in a SCEP response
func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) {
func (h *handler) GetCACert(ctx context.Context) (response, error) {
certs, err := h.Auth.GetCACertificates(ctx)
certs, err := h.auth.GetCACertificates(ctx)
if err != nil {
return SCEPResponse{}, err
return response{}, err
}
if len(certs) == 0 {
return SCEPResponse{}, errors.New("missing CA cert")
return response{}, errors.New("missing CA cert")
}
response := SCEPResponse{
res := response{
Operation: opnGetCACert,
CACertNum: len(certs),
}
if len(certs) == 1 {
response.Data = certs[0].Raw
res.Data = certs[0].Raw
} else {
// create degenerate pkcs7 certificate structure, according to
// https://tools.ietf.org/html/rfc8894#section-4.2.1.2, because
// not signed or encrypted data has to be returned.
data, err := microscep.DegenerateCertificates(certs)
if err != nil {
return SCEPResponse{}, err
return response{}, err
}
response.Data = data
res.Data = data
}
return response, nil
return res, nil
}
// GetCACaps returns the CA capabilities in a SCEP response
func (h *Handler) GetCACaps(ctx context.Context) (SCEPResponse, error) {
func (h *handler) GetCACaps(ctx context.Context) (response, error) {
caps := h.Auth.GetCACaps(ctx)
caps := h.auth.GetCACaps(ctx)
response := SCEPResponse{
res := response{
Operation: opnGetCACaps,
Data: formatCapabilities(caps),
}
return response, nil
return res, nil
}
// PKIOperation performs PKI operations and returns a SCEP response
func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPResponse, error) {
func (h *handler) PKIOperation(ctx context.Context, req request) (response, error) {
// parse the message using microscep implementation
microMsg, err := microscep.ParsePKIMessage(request.Message)
microMsg, err := microscep.ParsePKIMessage(req.Message)
if err != nil {
// return the error, because we can't use the msg for creating a CertRep
return SCEPResponse{}, err
return response{}, err
}
// this is essentially doing the same as microscep.ParsePKIMessage, but
@ -272,7 +268,7 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe
// wrapper for the microscep implementation.
p7, err := pkcs7.Parse(microMsg.Raw)
if err != nil {
return SCEPResponse{}, err
return response{}, err
}
// copy over properties to our internal PKIMessage
@ -284,8 +280,8 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe
P7: p7,
}
if err := h.Auth.DecryptPKIEnvelope(ctx, msg); err != nil {
return SCEPResponse{}, err
if err := h.auth.DecryptPKIEnvelope(ctx, msg); err != nil {
return response{}, err
}
// NOTE: at this point we have sufficient information for returning nicely signed CertReps
@ -297,7 +293,7 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe
// a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients.
// We'll have to see how it works out.
if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq {
challengeMatches, err := h.Auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword)
challengeMatches, err := h.auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword)
if err != nil {
return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password"))
}
@ -315,72 +311,67 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe
// Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification
// of the client cert is not.
certRep, err := h.Auth.SignCSR(ctx, csr, msg)
certRep, err := h.auth.SignCSR(ctx, csr, msg)
if err != nil {
return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.Wrap(err, "error when signing new certificate"))
return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err))
}
response := SCEPResponse{
res := response{
Operation: opnPKIOperation,
Data: certRep.Raw,
Certificate: certRep.Certificate,
}
return response, nil
return res, nil
}
func formatCapabilities(caps []string) []byte {
return []byte(strings.Join(caps, "\r\n"))
}
// writeSCEPResponse writes a SCEP response back to the SCEP client.
func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) {
// writeResponse writes a SCEP response back to the SCEP client.
func writeResponse(w http.ResponseWriter, res response) {
if response.Error != nil {
log.Error(w, response.Error)
if res.Error != nil {
log.Error(w, res.Error)
}
if response.Certificate != nil {
api.LogCertificate(w, response.Certificate)
if res.Certificate != nil {
api.LogCertificate(w, res.Certificate)
}
w.Header().Set("Content-Type", contentHeader(response))
_, err := w.Write(response.Data)
if err != nil {
writeError(w, errors.Wrap(err, "error when writing scep response")) // This could end up as an error again
}
w.Header().Set("Content-Type", contentHeader(res))
_, _ = w.Write(res.Data)
}
func writeError(w http.ResponseWriter, err error) {
scepError := &scep.Error{
Message: err.Error(),
Status: http.StatusInternalServerError, // TODO: make this a param?
}
api.WriteError(w, scepError)
func fail(w http.ResponseWriter, err error) {
log.Error(w, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func (h *Handler) createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (SCEPResponse, error) {
certRepMsg, err := h.Auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error())
func (h *handler) createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (response, error) {
certRepMsg, err := h.auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error())
if err != nil {
return SCEPResponse{}, err
return response{}, err
}
return SCEPResponse{
return response{
Operation: opnPKIOperation,
Data: certRepMsg.Raw,
Error: failError,
}, nil
}
func contentHeader(r SCEPResponse) string {
func contentHeader(r response) string {
switch r.Operation {
case opnGetCACert:
if r.CACertNum > 1 {
return certChainHeader
}
return leafHeader
case opnPKIOperation:
return pkiOperationHeader
default:
return "text/plain"
case opnGetCACert:
if r.CACertNum > 1 {
return "application/x-x509-ca-ra-cert"
}
return "application/x-x509-ca-cert"
case opnPKIOperation:
return "application/x-pki-message"
}
}

View file

@ -4,33 +4,19 @@ import (
"context"
"crypto/subtle"
"crypto/x509"
"errors"
"fmt"
"net/url"
"github.com/smallstep/certificates/authority/provisioner"
microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util"
microscep "github.com/micromdm/scep/v2/scep"
"github.com/pkg/errors"
"go.mozilla.org/pkcs7"
"go.step.sm/crypto/x509util"
"github.com/smallstep/certificates/authority/provisioner"
)
// Interface is the SCEP authority interface.
type Interface interface {
LoadProvisionerByName(string) (provisioner.Interface, error)
GetLinkExplicit(provName string, absoluteLink bool, baseURL *url.URL, inputs ...string) string
GetCACertificates(ctx context.Context) ([]*x509.Certificate, error)
DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error
SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error)
CreateFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error)
MatchChallengePassword(ctx context.Context, password string) (bool, error)
GetCACaps(ctx context.Context) []string
}
// Authority is the layer that handles all SCEP interactions.
type Authority struct {
prefix string
@ -180,12 +166,12 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
p7c, err := pkcs7.Parse(msg.P7.Content)
if err != nil {
return errors.Wrap(err, "error parsing pkcs7 content")
return fmt.Errorf("error parsing pkcs7 content: %w", err)
}
envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter)
if err != nil {
return errors.Wrap(err, "error decrypting encrypted pkcs7 content")
return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err)
}
msg.pkiEnvelope = envelope
@ -194,19 +180,19 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
case microscep.CertRep:
certs, err := microscep.CACerts(msg.pkiEnvelope)
if err != nil {
return errors.Wrap(err, "error extracting CA certs from pkcs7 degenerate data")
return fmt.Errorf("error extracting CA certs from pkcs7 degenerate data: %w", err)
}
msg.CertRepMessage.Certificate = certs[0]
return nil
case microscep.PKCSReq, microscep.UpdateReq, microscep.RenewalReq:
csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope)
if err != nil {
return errors.Wrap(err, "parse CSR from pkiEnvelope")
return fmt.Errorf("parse CSR from pkiEnvelope: %w", err)
}
// check for challengePassword
cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope)
if err != nil {
return errors.Wrap(err, "parse challenge password in pkiEnvelope")
return fmt.Errorf("parse challenge password in pkiEnvelope: %w", err)
}
msg.CSRReqMessage = &microscep.CSRReqMessage{
RawDecrypted: msg.pkiEnvelope,
@ -215,7 +201,7 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
}
return nil
case microscep.GetCRL, microscep.GetCert, microscep.CertPoll:
return errors.Errorf("not implemented")
return errors.New("not implemented")
}
return nil
@ -274,19 +260,19 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
signOps, err := p.AuthorizeSign(ctx, "")
if err != nil {
return nil, errors.Wrap(err, "error retrieving authorization options from SCEP provisioner")
return nil, fmt.Errorf("error retrieving authorization options from SCEP provisioner: %w", err)
}
opts := provisioner.SignOptions{}
templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)
if err != nil {
return nil, errors.Wrap(err, "error creating template options from SCEP provisioner")
return nil, fmt.Errorf("error creating template options from SCEP provisioner: %w", err)
}
signOps = append(signOps, templateOptions)
certChain, err := a.signAuth.Sign(csr, opts, signOps...)
if err != nil {
return nil, errors.Wrap(err, "error generating certificate for order")
return nil, fmt.Errorf("error generating certificate for order: %w", err)
}
// take the issued certificate (only); https://tools.ietf.org/html/rfc8894#section-3.3.2

View file

@ -1,12 +0,0 @@
package scep
// Error is an SCEP error type
type Error struct {
Message string `json:"message"`
Status int `json:"-"`
}
// Error implements the error interface.
func (e *Error) Error() string {
return e.Message
}

View file

@ -1,3 +1,4 @@
// Package scep implements Simple Certificate Enrollment Protocol related functionality.
package scep
import (
@ -5,9 +6,6 @@ import (
"encoding/asn1"
microscep "github.com/micromdm/scep/v2/scep"
//"github.com/smallstep/certificates/scep/pkcs7"
"go.mozilla.org/pkcs7"
)