forked from TrueCloudLab/certificates
Add rudimentary (and incomplete) support for SCEP
This commit is contained in:
parent
ff7b829aa2
commit
48c86716a0
10 changed files with 698 additions and 12 deletions
|
@ -442,9 +442,13 @@ func (a *Authority) init() error {
|
|||
audiences := a.config.GetAudiences()
|
||||
a.provisioners = provisioner.NewCollection(audiences)
|
||||
config := provisioner.Config{
|
||||
Claims: claimer.Claims(),
|
||||
Audiences: audiences,
|
||||
DB: a.db,
|
||||
// TODO: this probably shouldn't happen like this; via SignAuth instead?
|
||||
IntermediateCert: a.config.IntermediateCert,
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
CACertificates: a.rootX509Certs,
|
||||
Claims: claimer.Claims(),
|
||||
Audiences: audiences,
|
||||
DB: a.db,
|
||||
SSHKeys: &provisioner.SSHKeys{
|
||||
UserKeys: sshKeys.UserKeys,
|
||||
HostKeys: sshKeys.HostKeys,
|
||||
|
|
|
@ -142,6 +142,8 @@ const (
|
|||
TypeK8sSA Type = 8
|
||||
// TypeSSHPOP is used to indicate the SSHPOP provisioners.
|
||||
TypeSSHPOP Type = 9
|
||||
// TypeSCEP is used to indicate the SCEP provisioners
|
||||
TypeSCEP Type = 10
|
||||
)
|
||||
|
||||
// String returns the string representation of the type.
|
||||
|
@ -165,6 +167,8 @@ func (t Type) String() string {
|
|||
return "K8sSA"
|
||||
case TypeSSHPOP:
|
||||
return "SSHPOP"
|
||||
case TypeSCEP:
|
||||
return "SCEP"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -179,6 +183,10 @@ type SSHKeys struct {
|
|||
// Config defines the default parameters used in the initialization of
|
||||
// provisioners.
|
||||
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 Claims
|
||||
// Audiences are the audiences used in the default provisioner, (JWK).
|
||||
|
@ -233,6 +241,8 @@ func (l *List) UnmarshalJSON(data []byte) error {
|
|||
p = &K8sSA{}
|
||||
case "sshpop":
|
||||
p = &SSHPOP{}
|
||||
case "scep":
|
||||
p = &SCEP{}
|
||||
default:
|
||||
// Skip unsupported provisioners. A client using this method may be
|
||||
// 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)
|
||||
)
|
61
ca/ca.go
61
ca/ca.go
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/certificates/monitoring"
|
||||
"github.com/smallstep/certificates/scep"
|
||||
"github.com/smallstep/certificates/server"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
@ -179,17 +180,39 @@ func (ca *CA) Init(config *config.Config) (*CA, error) {
|
|||
mgmtHandler.Route(r)
|
||||
})
|
||||
}
|
||||
if ca.shouldServeSCEPEndpoints() {
|
||||
|
||||
scepPrefix := "scep"
|
||||
scepAuthority, err := scep.New(auth, scep.AuthorityOptions{
|
||||
Service: auth.GetSCEPService(),
|
||||
DNS: dns,
|
||||
Prefix: scepPrefix,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating SCEP authority")
|
||||
}
|
||||
scepRouterHandler := scepAPI.New(scepAuthority)
|
||||
|
||||
// According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10),
|
||||
// SCEP operations are performed using HTTP, so that's why the API is mounted
|
||||
// to the insecure mux.
|
||||
insecureMux.Route("/"+scepPrefix, func(r chi.Router) {
|
||||
scepRouterHandler.Route(r)
|
||||
})
|
||||
|
||||
// The RFC also mentions usage of HTTPS, but seems to advise
|
||||
// against it, because of potential interoperability issues.
|
||||
// Currently I think it's not bad to use HTTPS also, so that's
|
||||
// why I've kept the API endpoints in both muxes and both HTTP
|
||||
// as well as HTTPS can be used to request certificates
|
||||
// using SCEP.
|
||||
mux.Route("/"+scepPrefix, func(r chi.Router) {
|
||||
scepRouterHandler.Route(r)
|
||||
})
|
||||
}
|
||||
|
||||
// helpful routine for logging all routes //
|
||||
/*
|
||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
fmt.Printf("%s %s\n", method, route)
|
||||
return nil
|
||||
}
|
||||
if err := chi.Walk(mux, walkFunc); err != nil {
|
||||
fmt.Printf("Logging err: %s\n", err.Error())
|
||||
}
|
||||
*/
|
||||
//dumpRoutes(mux)
|
||||
|
||||
// Add monitoring if configured
|
||||
if len(config.Monitoring) > 0 {
|
||||
|
@ -331,3 +354,23 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// shouldMountSCEPEndpoints returns if the CA should be
|
||||
// configured with endpoints for SCEP. This is assumed to be
|
||||
// true if a SCEPService exists, which is true in case a
|
||||
// SCEP provisioner was configured.
|
||||
func (ca *CA) shouldServeSCEPEndpoints() bool {
|
||||
return ca.auth.GetSCEPService() != nil
|
||||
}
|
||||
|
||||
//nolint // ignore linters to allow keeping this function around for debugging
|
||||
func dumpRoutes(mux chi.Routes) {
|
||||
// helpful routine for logging all routes //
|
||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
fmt.Printf("%s %s\n", method, route)
|
||||
return nil
|
||||
}
|
||||
if err := chi.Walk(mux, walkFunc); err != nil {
|
||||
fmt.Printf("Logging err: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -13,6 +13,7 @@ require (
|
|||
github.com/google/uuid v1.1.2
|
||||
github.com/googleapis/gax-go/v2 v2.0.5
|
||||
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/pkg/errors v0.9.1
|
||||
github.com/rs/xid v1.2.1
|
||||
|
|
2
go.sum
2
go.sum
|
@ -234,6 +234,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
|
|||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ=
|
||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
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/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
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