forked from TrueCloudLab/certificates
Add rudimentary (and incomplete) support for SCEP
This commit is contained in:
parent
fc93d60c5d
commit
ffdd58ea3c
10 changed files with 668 additions and 3 deletions
|
@ -329,9 +329,13 @@ func (a *Authority) init() error {
|
||||||
audiences := a.config.getAudiences()
|
audiences := a.config.getAudiences()
|
||||||
a.provisioners = provisioner.NewCollection(audiences)
|
a.provisioners = provisioner.NewCollection(audiences)
|
||||||
config := provisioner.Config{
|
config := provisioner.Config{
|
||||||
Claims: claimer.Claims(),
|
// TODO: this probably shouldn't happen like this; via SignAuth instead?
|
||||||
Audiences: audiences,
|
IntermediateCert: a.config.IntermediateCert,
|
||||||
DB: a.db,
|
SigningKey: a.config.IntermediateKey,
|
||||||
|
CACertificates: a.rootX509Certs,
|
||||||
|
Claims: claimer.Claims(),
|
||||||
|
Audiences: audiences,
|
||||||
|
DB: a.db,
|
||||||
SSHKeys: &provisioner.SSHKeys{
|
SSHKeys: &provisioner.SSHKeys{
|
||||||
UserKeys: sshKeys.UserKeys,
|
UserKeys: sshKeys.UserKeys,
|
||||||
HostKeys: sshKeys.HostKeys,
|
HostKeys: sshKeys.HostKeys,
|
||||||
|
|
|
@ -141,6 +141,8 @@ const (
|
||||||
TypeK8sSA Type = 8
|
TypeK8sSA Type = 8
|
||||||
// TypeSSHPOP is used to indicate the SSHPOP provisioners.
|
// TypeSSHPOP is used to indicate the SSHPOP provisioners.
|
||||||
TypeSSHPOP Type = 9
|
TypeSSHPOP Type = 9
|
||||||
|
// TypeSCEP is used to indicate the SCEP provisioners
|
||||||
|
TypeSCEP Type = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns the string representation of the type.
|
// String returns the string representation of the type.
|
||||||
|
@ -164,6 +166,8 @@ func (t Type) String() string {
|
||||||
return "K8sSA"
|
return "K8sSA"
|
||||||
case TypeSSHPOP:
|
case TypeSSHPOP:
|
||||||
return "SSHPOP"
|
return "SSHPOP"
|
||||||
|
case TypeSCEP:
|
||||||
|
return "SCEP"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -178,6 +182,10 @@ type SSHKeys struct {
|
||||||
// Config defines the default parameters used in the initialization of
|
// Config defines the default parameters used in the initialization of
|
||||||
// provisioners.
|
// provisioners.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// TODO: these probably shouldn't be here but passed via SignAuth
|
||||||
|
IntermediateCert string
|
||||||
|
SigningKey string
|
||||||
|
CACertificates []*x509.Certificate
|
||||||
// Claims are the default claims.
|
// Claims are the default claims.
|
||||||
Claims Claims
|
Claims Claims
|
||||||
// Audiences are the audiences used in the default provisioner, (JWK).
|
// Audiences are the audiences used in the default provisioner, (JWK).
|
||||||
|
@ -232,6 +240,8 @@ func (l *List) UnmarshalJSON(data []byte) error {
|
||||||
p = &K8sSA{}
|
p = &K8sSA{}
|
||||||
case "sshpop":
|
case "sshpop":
|
||||||
p = &SSHPOP{}
|
p = &SSHPOP{}
|
||||||
|
case "scep":
|
||||||
|
p = &SCEP{}
|
||||||
default:
|
default:
|
||||||
// Skip unsupported provisioners. A client using this method may be
|
// Skip unsupported provisioners. A client using this method may be
|
||||||
// compiled with a version of smallstep/certificates that does not
|
// compiled with a version of smallstep/certificates that does not
|
||||||
|
|
116
authority/provisioner/scep.go
Normal file
116
authority/provisioner/scep.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package provisioner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SCEP is the SCEP provisioner type, an entity that can authorize the
|
||||||
|
// SCEP provisioning flow
|
||||||
|
type SCEP struct {
|
||||||
|
*base
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
// ForceCN bool `json:"forceCN,omitempty"`
|
||||||
|
// Claims *Claims `json:"claims,omitempty"`
|
||||||
|
// Options *Options `json:"options,omitempty"`
|
||||||
|
// claimer *Claimer
|
||||||
|
|
||||||
|
IntermediateCert string
|
||||||
|
SigningKey string
|
||||||
|
CACertificates []*x509.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the provisioner unique identifier.
|
||||||
|
func (s SCEP) GetID() string {
|
||||||
|
return "scep/" + s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the provisioner.
|
||||||
|
func (s *SCEP) GetName() string {
|
||||||
|
return s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the type of provisioner.
|
||||||
|
func (s *SCEP) GetType() Type {
|
||||||
|
return TypeSCEP
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
|
||||||
|
func (s *SCEP) GetEncryptedKey() (string, string, bool) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenID returns the identifier of the token.
|
||||||
|
func (s *SCEP) GetTokenID(ott string) (string, error) {
|
||||||
|
return "", errors.New("scep provisioner does not implement GetTokenID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCACertificates returns the CA certificate chain
|
||||||
|
// TODO: this should come from the authority instead?
|
||||||
|
func (s *SCEP) GetCACertificates() []*x509.Certificate {
|
||||||
|
|
||||||
|
pemtxt, _ := ioutil.ReadFile(s.IntermediateCert) // TODO: move reading key to init? That's probably safer.
|
||||||
|
block, _ := pem.Decode([]byte(pemtxt))
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return chain? I'm not sure if the client understands it correctly
|
||||||
|
return []*x509.Certificate{cert}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SCEP) GetSigningKey() *rsa.PrivateKey {
|
||||||
|
|
||||||
|
keyBytes, err := ioutil.ReadFile(s.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode([]byte(keyBytes))
|
||||||
|
if block == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes and validates the fields of a JWK type.
|
||||||
|
func (s *SCEP) Init(config Config) (err error) {
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case s.Type == "":
|
||||||
|
return errors.New("provisioner type cannot be empty")
|
||||||
|
case s.Name == "":
|
||||||
|
return errors.New("provisioner name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Update claims with global ones
|
||||||
|
// if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
s.IntermediateCert = config.IntermediateCert
|
||||||
|
s.SigningKey = config.SigningKey
|
||||||
|
s.CACertificates = config.CACertificates
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var (
|
||||||
|
_ Interface = (*SCEP)(nil)
|
||||||
|
//_ scep.Provisioner = (*SCEP)(nil)
|
||||||
|
)
|
22
ca/ca.go
22
ca/ca.go
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/logging"
|
"github.com/smallstep/certificates/logging"
|
||||||
"github.com/smallstep/certificates/monitoring"
|
"github.com/smallstep/certificates/monitoring"
|
||||||
|
"github.com/smallstep/certificates/scep"
|
||||||
"github.com/smallstep/certificates/server"
|
"github.com/smallstep/certificates/server"
|
||||||
"github.com/smallstep/nosql"
|
"github.com/smallstep/nosql"
|
||||||
)
|
)
|
||||||
|
@ -143,6 +144,27 @@ func (ca *CA) Init(config *authority.Config) (*CA, error) {
|
||||||
acmeRouterHandler.Route(r)
|
acmeRouterHandler.Route(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: THIS SHOULDN'T HAPPEN (or should become configurable)
|
||||||
|
// Current SCEP client I'm testing with doesn't seem to easily trust untrusted certs.
|
||||||
|
tlsConfig = nil
|
||||||
|
|
||||||
|
scepPrefix := "scep"
|
||||||
|
scepAuthority, err := scep.New(auth, scep.AuthorityOptions{
|
||||||
|
//Certificates: certificates,
|
||||||
|
//AuthConfig: *config.AuthorityConfig,
|
||||||
|
//Backdate: *config.AuthorityConfig.Backdate,
|
||||||
|
DB: auth.GetDatabase().(nosql.DB),
|
||||||
|
DNS: dns,
|
||||||
|
Prefix: scepPrefix,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error creating SCEP authority")
|
||||||
|
}
|
||||||
|
scepRouterHandler := scep.NewAPI(scepAuthority)
|
||||||
|
mux.Route("/"+scepPrefix, func(r chi.Router) {
|
||||||
|
scepRouterHandler.Route(r)
|
||||||
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// helpful routine for logging all routes //
|
// helpful routine for logging all routes //
|
||||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -12,6 +12,7 @@ require (
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5
|
github.com/googleapis/gax-go/v2 v2.0.5
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/micromdm/scep v1.0.0
|
||||||
github.com/newrelic/go-agent v2.15.0+incompatible
|
github.com/newrelic/go-agent v2.15.0+incompatible
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rs/xid v1.2.1
|
github.com/rs/xid v1.2.1
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -214,6 +214,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/micromdm/scep v1.0.0 h1:ai//kcZnxZPq1YE/MatiE2bIRD94KOAwZRpN1fhVQXY=
|
||||||
|
github.com/micromdm/scep v1.0.0/go.mod h1:CID2SixSr5FvoauZdAFUSpQkn5MAuSy9oyURMGOJbag=
|
||||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
|
367
scep/api.go
Normal file
367
scep/api.go
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
package scep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
|
"github.com/smallstep/certificates/api"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
|
||||||
|
microscep "github.com/micromdm/scep/scep"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler is the ACME request handler.
|
||||||
|
type Handler struct {
|
||||||
|
Auth Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new ACME API router.
|
||||||
|
func NewAPI(scepAuth Interface) api.RouterHandler {
|
||||||
|
return &Handler{scepAuth}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route traffic and implement the Router interface.
|
||||||
|
func (h *Handler) Route(r api.Router) {
|
||||||
|
//getLink := h.Auth.GetLinkExplicit
|
||||||
|
//fmt.Println(getLink)
|
||||||
|
|
||||||
|
//r.MethodFunc("GET", "/bla", h.baseURLFromRequest(h.lookupProvisioner(nil)))
|
||||||
|
//r.MethodFunc("GET", getLink(acme.NewNonceLink, "{provisionerID}", false, nil), h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.GetNonce))))
|
||||||
|
|
||||||
|
r.MethodFunc(http.MethodGet, "/", h.lookupProvisioner(h.Get))
|
||||||
|
r.MethodFunc(http.MethodPost, "/", h.lookupProvisioner(h.Post))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
scepRequest, err := decodeSCEPRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Println("not a scep get request")
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
scepResponse := SCEPResponse{Operation: scepRequest.Operation}
|
||||||
|
|
||||||
|
switch scepRequest.Operation {
|
||||||
|
case opnGetCACert:
|
||||||
|
err := h.GetCACert(w, r, scepResponse)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case opnGetCACaps:
|
||||||
|
err := h.GetCACaps(w, r, scepResponse)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
case opnPKIOperation:
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
|
||||||
|
scepRequest, err := decodeSCEPRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Println("not a scep post request")
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
scepResponse := SCEPResponse{Operation: scepRequest.Operation}
|
||||||
|
|
||||||
|
switch scepRequest.Operation {
|
||||||
|
case opnPKIOperation:
|
||||||
|
err := h.PKIOperation(w, r, scepRequest, scepResponse)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPayloadSize = 2 << 20
|
||||||
|
|
||||||
|
func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
method := r.Method
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
var operation string
|
||||||
|
if _, ok := query["operation"]; ok {
|
||||||
|
operation = query.Get("operation")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case http.MethodGet:
|
||||||
|
switch operation {
|
||||||
|
case opnGetCACert, opnGetCACaps:
|
||||||
|
return SCEPRequest{
|
||||||
|
Operation: operation,
|
||||||
|
Message: []byte{},
|
||||||
|
}, nil
|
||||||
|
case opnPKIOperation:
|
||||||
|
var message string
|
||||||
|
if _, ok := query["message"]; ok {
|
||||||
|
message = query.Get("message")
|
||||||
|
}
|
||||||
|
decodedMessage, err := base64.URLEncoding.DecodeString(message)
|
||||||
|
if err != nil {
|
||||||
|
return SCEPRequest{}, err
|
||||||
|
}
|
||||||
|
return SCEPRequest{
|
||||||
|
Operation: operation,
|
||||||
|
Message: decodedMessage,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return SCEPRequest{}, fmt.Errorf("unsupported operation: %s", operation)
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, maxPayloadSize))
|
||||||
|
if err != nil {
|
||||||
|
return SCEPRequest{}, err
|
||||||
|
}
|
||||||
|
return SCEPRequest{
|
||||||
|
Operation: operation,
|
||||||
|
Message: body,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return SCEPRequest{}, fmt.Errorf("unsupported method: %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
// lookupProvisioner loads the provisioner associated with the request.
|
||||||
|
// Responds 404 if the provisioner does not exist.
|
||||||
|
func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// TODO: make this configurable; and we might want to look at being able to provide multiple,
|
||||||
|
// like the actual ACME one? The below assumes a SCEP provider (scep/) called "scep1" exists.
|
||||||
|
p, err := h.Auth.LoadProvisionerByID("scep/scep1")
|
||||||
|
if err != nil {
|
||||||
|
api.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scepProvisioner, ok := p.(*provisioner.SCEP)
|
||||||
|
if !ok {
|
||||||
|
api.WriteError(w, acme.AccountDoesNotExistErr(errors.New("provisioner must be of type SCEP")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, acme.ProvisionerContextKey, Provisioner(scepProvisioner))
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetCACert(w http.ResponseWriter, r *http.Request, scepResponse SCEPResponse) error {
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
p, err := ProvisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: get the CA Certificates from the (signing) authority instead? I think that should be doable
|
||||||
|
certs := p.GetCACertificates()
|
||||||
|
|
||||||
|
if len(certs) == 0 {
|
||||||
|
scepResponse.CACertNum = 0
|
||||||
|
scepResponse.Err = errors.New("missing CA Cert")
|
||||||
|
} else if len(certs) == 1 {
|
||||||
|
scepResponse.Data = certs[0].Raw
|
||||||
|
scepResponse.CACertNum = 1
|
||||||
|
} else {
|
||||||
|
data, err := microscep.DegenerateCertificates(certs)
|
||||||
|
scepResponse.Data = data
|
||||||
|
scepResponse.Err = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeSCEPResponse(w, scepResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetCACaps(w http.ResponseWriter, r *http.Request, scepResponse SCEPResponse) error {
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
_, err := ProvisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: get the actual capabilities from provisioner config
|
||||||
|
scepResponse.Data = formatCapabilities(defaultCapabilities)
|
||||||
|
|
||||||
|
return writeSCEPResponse(w, scepResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) PKIOperation(w http.ResponseWriter, r *http.Request, scepRequest SCEPRequest, scepResponse SCEPResponse) error {
|
||||||
|
|
||||||
|
msg, err := microscep.ParsePKIMessage(scepRequest.Message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
p, err := ProvisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
certs := p.GetCACertificates()
|
||||||
|
key := p.GetSigningKey()
|
||||||
|
|
||||||
|
ca := certs[0]
|
||||||
|
if err := msg.DecryptPKIEnvelope(ca, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.MessageType == microscep.PKCSReq {
|
||||||
|
// TODO: CSR validation, like challenge password
|
||||||
|
}
|
||||||
|
|
||||||
|
csr := msg.CSRReqMessage.CSR
|
||||||
|
id, err := createKeyIdentifier(csr.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := big.NewInt(int64(rand.Int63())) // TODO: serial logic?
|
||||||
|
|
||||||
|
days := 40
|
||||||
|
|
||||||
|
template := &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: csr.Subject,
|
||||||
|
NotBefore: time.Now().Add(-600).UTC(),
|
||||||
|
NotAfter: time.Now().AddDate(0, 0, days).UTC(),
|
||||||
|
SubjectKeyId: id,
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
|
x509.ExtKeyUsageClientAuth,
|
||||||
|
},
|
||||||
|
SignatureAlgorithm: csr.SignatureAlgorithm,
|
||||||
|
EmailAddresses: csr.EmailAddresses,
|
||||||
|
}
|
||||||
|
|
||||||
|
certRep, err := msg.SignCSR(ca, key, template)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//cert := certRep.CertRepMessage.Certificate
|
||||||
|
//name := certName(cert)
|
||||||
|
|
||||||
|
// TODO: check if CN already exists, if renewal is allowed and if existing should be revoked; fail if not
|
||||||
|
// TODO: store the new cert for CN locally
|
||||||
|
|
||||||
|
scepResponse.Data = certRep.Raw
|
||||||
|
|
||||||
|
return writeSCEPResponse(w, scepResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func certName(cert *x509.Certificate) string {
|
||||||
|
if cert.Subject.CommonName != "" {
|
||||||
|
return cert.Subject.CommonName
|
||||||
|
}
|
||||||
|
return string(cert.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createKeyIdentifier create an identifier for public keys
|
||||||
|
// according to the first method in RFC5280 section 4.2.1.2.
|
||||||
|
func createKeyIdentifier(pub crypto.PublicKey) ([]byte, error) {
|
||||||
|
|
||||||
|
keyBytes, err := x509.MarshalPKIXPublicKey(pub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id := sha1.Sum(keyBytes)
|
||||||
|
|
||||||
|
return id[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatCapabilities(caps []string) []byte {
|
||||||
|
return []byte(strings.Join(caps, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSCEPResponse writes a SCEP response back to the SCEP client.
|
||||||
|
func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) error {
|
||||||
|
if response.Err != nil {
|
||||||
|
http.Error(w, response.Err.Error(), http.StatusInternalServerError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", contentHeader(response.Operation, response.CACertNum))
|
||||||
|
w.Write(response.Data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TODO: check the default capabilities
|
||||||
|
defaultCapabilities = []string{
|
||||||
|
"Renewal",
|
||||||
|
"SHA-1",
|
||||||
|
"SHA-256",
|
||||||
|
"AES",
|
||||||
|
"DES3",
|
||||||
|
"SCEPStandard",
|
||||||
|
"POSTPKIOperation",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certChainHeader = "application/x-x509-ca-ra-cert"
|
||||||
|
leafHeader = "application/x-x509-ca-cert"
|
||||||
|
pkiOpHeader = "application/x-pki-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func contentHeader(operation string, certNum int) string {
|
||||||
|
switch operation {
|
||||||
|
case opnGetCACert:
|
||||||
|
if certNum > 1 {
|
||||||
|
return certChainHeader
|
||||||
|
}
|
||||||
|
return leafHeader
|
||||||
|
case opnPKIOperation:
|
||||||
|
return pkiOpHeader
|
||||||
|
default:
|
||||||
|
return "text/plain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisionerFromContext searches the context for a provisioner. Returns the
|
||||||
|
// provisioner or an error.
|
||||||
|
func ProvisionerFromContext(ctx context.Context) (Provisioner, error) {
|
||||||
|
val := ctx.Value(acme.ProvisionerContextKey)
|
||||||
|
if val == nil {
|
||||||
|
return nil, acme.ServerInternalErr(errors.New("provisioner expected in request context"))
|
||||||
|
}
|
||||||
|
pval, ok := val.(Provisioner)
|
||||||
|
if !ok || pval == nil {
|
||||||
|
return nil, acme.ServerInternalErr(errors.New("provisioner in context is not a SCEP provisioner"))
|
||||||
|
}
|
||||||
|
return pval, nil
|
||||||
|
}
|
88
scep/authority.go
Normal file
88
scep/authority.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package scep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
|
||||||
|
"github.com/smallstep/nosql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface is the SCEP authority interface.
|
||||||
|
type Interface interface {
|
||||||
|
// GetDirectory(ctx context.Context) (*Directory, error)
|
||||||
|
// NewNonce() (string, error)
|
||||||
|
// UseNonce(string) error
|
||||||
|
|
||||||
|
// DeactivateAccount(ctx context.Context, accID string) (*Account, error)
|
||||||
|
// GetAccount(ctx context.Context, accID string) (*Account, error)
|
||||||
|
// GetAccountByKey(ctx context.Context, key *jose.JSONWebKey) (*Account, error)
|
||||||
|
// NewAccount(ctx context.Context, ao AccountOptions) (*Account, error)
|
||||||
|
// UpdateAccount(context.Context, string, []string) (*Account, error)
|
||||||
|
|
||||||
|
// GetAuthz(ctx context.Context, accID string, authzID string) (*Authz, error)
|
||||||
|
// ValidateChallenge(ctx context.Context, accID string, chID string, key *jose.JSONWebKey) (*Challenge, error)
|
||||||
|
|
||||||
|
// FinalizeOrder(ctx context.Context, accID string, orderID string, csr *x509.CertificateRequest) (*Order, error)
|
||||||
|
// GetOrder(ctx context.Context, accID string, orderID string) (*Order, error)
|
||||||
|
// GetOrdersByAccount(ctx context.Context, accID string) ([]string, error)
|
||||||
|
// NewOrder(ctx context.Context, oo OrderOptions) (*Order, error)
|
||||||
|
|
||||||
|
// GetCertificate(string, string) ([]byte, error)
|
||||||
|
|
||||||
|
LoadProvisionerByID(string) (provisioner.Interface, error)
|
||||||
|
// GetLink(ctx context.Context, linkType Link, absoluteLink bool, inputs ...string) string
|
||||||
|
// GetLinkExplicit(linkType Link, provName string, absoluteLink bool, baseURL *url.URL, inputs ...string) string
|
||||||
|
|
||||||
|
GetCACerts() ([]*x509.Certificate, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authority is the layer that handles all SCEP interactions.
|
||||||
|
type Authority struct {
|
||||||
|
//certificates []*x509.Certificate
|
||||||
|
//authConfig authority.AuthConfig
|
||||||
|
backdate provisioner.Duration
|
||||||
|
db nosql.DB
|
||||||
|
// dir *directory
|
||||||
|
signAuth SignAuthority
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorityOptions required to create a new SCEP Authority.
|
||||||
|
type AuthorityOptions struct {
|
||||||
|
Certificates []*x509.Certificate
|
||||||
|
//AuthConfig authority.AuthConfig
|
||||||
|
Backdate provisioner.Duration
|
||||||
|
// DB is the database used by nosql.
|
||||||
|
DB nosql.DB
|
||||||
|
// DNS the host used to generate accurate SCEP links. By default the authority
|
||||||
|
// will use the Host from the request, so this value will only be used if
|
||||||
|
// request.Host is empty.
|
||||||
|
DNS string
|
||||||
|
// Prefix is a URL path prefix under which the SCEP api is served. This
|
||||||
|
// prefix is required to generate accurate SCEP links.
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignAuthority is the interface implemented by a CA authority.
|
||||||
|
type SignAuthority interface {
|
||||||
|
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||||
|
LoadProvisionerByID(string) (provisioner.Interface, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadProvisionerByID calls out to the SignAuthority interface to load a
|
||||||
|
// provisioner by ID.
|
||||||
|
func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error) {
|
||||||
|
return a.signAuth.LoadProvisionerByID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) GetCACerts() ([]*x509.Certificate, error) {
|
||||||
|
|
||||||
|
// TODO: implement the SCEP authority
|
||||||
|
|
||||||
|
return []*x509.Certificate{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var (
|
||||||
|
_ Interface = (*Authority)(nil)
|
||||||
|
)
|
17
scep/provisioner.go
Normal file
17
scep/provisioner.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package scep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provisioner is an interface that implements a subset of the provisioner.Interface --
|
||||||
|
// only those methods required by the SCEP api/authority.
|
||||||
|
type Provisioner interface {
|
||||||
|
// AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
|
||||||
|
// GetName() string
|
||||||
|
// DefaultTLSCertDuration() time.Duration
|
||||||
|
// GetOptions() *provisioner.Options
|
||||||
|
GetCACertificates() []*x509.Certificate
|
||||||
|
GetSigningKey() *rsa.PrivateKey
|
||||||
|
}
|
38
scep/scep.go
Normal file
38
scep/scep.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package scep
|
||||||
|
|
||||||
|
import (
|
||||||
|
database "github.com/smallstep/certificates/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
opnGetCACert = "GetCACert"
|
||||||
|
opnGetCACaps = "GetCACaps"
|
||||||
|
opnPKIOperation = "PKIOperation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new Authority that implements the SCEP interface.
|
||||||
|
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
|
||||||
|
if _, ok := ops.DB.(*database.SimpleDB); !ok {
|
||||||
|
// TODO: see ACME implementation
|
||||||
|
}
|
||||||
|
return &Authority{
|
||||||
|
//certificates: ops.Certificates,
|
||||||
|
backdate: ops.Backdate,
|
||||||
|
db: ops.DB,
|
||||||
|
signAuth: signAuth,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SCEPRequest is a SCEP server request.
|
||||||
|
type SCEPRequest struct {
|
||||||
|
Operation string
|
||||||
|
Message []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SCEPResponse is a SCEP server response.
|
||||||
|
type SCEPResponse struct {
|
||||||
|
Operation string
|
||||||
|
CACertNum int
|
||||||
|
Data []byte
|
||||||
|
Err error
|
||||||
|
}
|
Loading…
Reference in a new issue