Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggable CSR signing framework #113

Merged
merged 3 commits into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ deps: gomodcheck
@go mod download

test:
go test -cover ./...

testrace:
go test -cover -race ./...

build: build-scepclient build-scepserver
Expand Down
29 changes: 29 additions & 0 deletions challenge/challenge.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
// Package challenge defines an interface for a dynamic challenge password cache.
package challenge

import (
"crypto/x509"
"errors"

"github.com/micromdm/scep/scep"
scepserver "github.com/micromdm/scep/server"
)

// Store is a dynamic challenge password cache.
type Store interface {
SCEPChallenge() (string, error)
HasChallenge(pw string) (bool, error)
}

func csrSignerMiddleWare(store Store, next scepserver.CSRSigner) scepserver.CSRSignerFunc {
return func(m *scep.CSRReqMessage) (*x509.Certificate, error) {
// TODO: this was only verified in the old version if our MessageType was PKCSReq
valid, err := store.HasChallenge(m.ChallengePassword)
if err != nil {
return nil, err
}
if !valid {
return nil, errors.New("invalid SCEP challenge")
}
return next.SignCSR(m)
}
}

// NewCSRSignerMiddleware creates a new middleware adaptor
func NewCSRSignerMiddleware(store Store) func(scepserver.CSRSigner) scepserver.CSRSigner {
return func(f scepserver.CSRSigner) scepserver.CSRSigner {
return csrSignerMiddleWare(store, f)
}
}
100 changes: 100 additions & 0 deletions challenge/challenge_bolt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package challenge

import (
"crypto/x509"
"io/ioutil"
"os"
"testing"

"github.com/boltdb/bolt"
challengestore "github.com/micromdm/scep/challenge/bolt"
"github.com/micromdm/scep/scep"
scepserver "github.com/micromdm/scep/server"
)

func TestDynamicChallenge(t *testing.T) {
db, err := openTempBolt("scep-challenge")
if err != nil {
t.Fatal(err)
}

depot, err := challengestore.NewBoltDepot(db)
if err != nil {
t.Fatal(err)
}

// use the exported interface
store := Store(depot)

// get first challenge
challengePassword, err := store.SCEPChallenge()
if err != nil {
t.Fatal(err)
}

if challengePassword == "" {
t.Error("empty challenge returned")
}

// test store API
valid, err := store.HasChallenge(challengePassword)
if err != nil {
t.Fatal(err)
}
if valid != true {
t.Error("challenge just acquired is not valid")
}
valid, err = store.HasChallenge(challengePassword)
if err != nil {
t.Fatal(err)
}
if valid != false {
t.Error("challenge should not be valid twice")
}

// get another challenge
challengePassword, err = store.SCEPChallenge()
if err != nil {
t.Fatal(err)
}

if challengePassword == "" {
t.Error("empty challenge returned")
}

// test CSRSigner middleware
nullSigner := scepserver.CSRSignerFunc(func(*scep.CSRReqMessage) (*x509.Certificate, error) {
return nil, nil
})
mw := NewCSRSignerMiddleware(depot)
signer := mw(nullSigner)

csrReq := &scep.CSRReqMessage{
ChallengePassword: challengePassword,
}

_, err = signer.SignCSR(csrReq)
if err != nil {
t.Error(err)
}

_, err = signer.SignCSR(csrReq)
if err == nil {
t.Error("challenge should not be valid twice")
}

}

func openTempBolt(prefix string) (*bolt.DB, error) {
f, err := ioutil.TempFile("", prefix+"-")
if err != nil {
return nil, err
}
f.Close()
err = os.Remove(f.Name())
if err != nil {
return nil, err
}

return bolt.Open(f.Name(), 0644, nil)
}
33 changes: 22 additions & 11 deletions cmd/scepserver/scepserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import (
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/micromdm/scep/csrverifier"
"github.com/micromdm/scep/csrverifier/executable"
"github.com/micromdm/scep/depot"
executablecsrverifier "github.com/micromdm/scep/csrverifier/executable"
scepdepot "github.com/micromdm/scep/depot"
"github.com/micromdm/scep/depot/file"
"github.com/micromdm/scep/server"
scepserver "github.com/micromdm/scep/server"
)

// version info
Expand Down Expand Up @@ -94,7 +94,7 @@ func main() {
lginfo := level.Info(logger)

var err error
var depot depot.Depot // cert storage
var depot scepdepot.Depot // cert storage
{
depot, err = file.NewFileDepot(*flDepotPath)
if err != nil {
Expand Down Expand Up @@ -125,14 +125,25 @@ func main() {
var svc scepserver.Service // scep service
{
svcOptions := []scepserver.ServiceOption{
scepserver.ChallengePassword(*flChallengePassword),
scepserver.WithCSRVerifier(csrVerifier),
scepserver.CAKeyPassword([]byte(*flCAPass)),
scepserver.ClientValidity(clientValidity),
scepserver.AllowRenewal(allowRenewal),
scepserver.WithLogger(logger),
}
svc, err = scepserver.NewService(depot, svcOptions...)
if *flChallengePassword != "" {
svcOptions = append(svcOptions, scepserver.WithStaticChallengePassword(*flChallengePassword))
}
if csrVerifier != nil {
svcOptions = append(svcOptions, scepserver.WithCSRSignerMiddleware(csrverifier.NewCSRSignerMiddleware(csrVerifier)))
}
crts, key, err := depot.CA([]byte(*flCAPass))
if err != nil {
lginfo.Log("err", err)
os.Exit(1)
}
if len(crts) < 1 {
lginfo.Log("err", "missing CA certificate")
os.Exit(1)
}
signer := scepdepot.CSRSigner(depot, allowRenewal, clientValidity, *flCAPass)
svc, err = scepserver.NewService(crts[0], key, signer, svcOptions...)
if err != nil {
lginfo.Log("err", err)
os.Exit(1)
Expand Down Expand Up @@ -255,7 +266,7 @@ func createCertificateAuthority(key *rsa.PrivateKey, years int, organization str

// activate CA
BasicConstraintsValid: true,
IsCA: true,
IsCA: true,
// Not allow any non-self-issued intermediate CA
MaxPathLen: 0,

Expand Down
30 changes: 29 additions & 1 deletion csrverifier/csrverifier.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
// Package csrverifier defines an interface for CSR verification.
package csrverifier

// Verify the raw decrypted CSR.
import (
"crypto/x509"
"errors"

"github.com/micromdm/scep/scep"
scepserver "github.com/micromdm/scep/server"
)

// CSRVerifier verifies the raw decrypted CSR.
type CSRVerifier interface {
Verify(data []byte) (bool, error)
}

func csrSignerMiddleWare(verifier CSRVerifier, next scepserver.CSRSigner) scepserver.CSRSignerFunc {
return func(m *scep.CSRReqMessage) (*x509.Certificate, error) {
result, err := verifier.Verify(m.RawDecrypted)
if err != nil {
return nil, err
}
if !result {
return nil, errors.New("CSR failed verification")
}
return next.SignCSR(m)
}
}

// NewCSRSignerMiddleware creates a new middleware adaptor
func NewCSRSignerMiddleware(verifier CSRVerifier) func(scepserver.CSRSigner) scepserver.CSRSigner {
return func(f scepserver.CSRSigner) scepserver.CSRSigner {
return csrSignerMiddleWare(verifier, f)
}
}
113 changes: 113 additions & 0 deletions depot/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package depot

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/asn1"
"errors"
"math/big"
"time"

scepserver "github.com/micromdm/scep/server"

"github.com/micromdm/scep/scep"
)

// CSRSigner returns a CSRSignerFunc for use in new scepserver service
func CSRSigner(depot Depot, allowRenewal, clientValidity int, caPass string) scepserver.CSRSignerFunc {
return func(m *scep.CSRReqMessage) (*x509.Certificate, error) {
csr := m.CSR
id, err := generateSubjectKeyID(csr.PublicKey)
if err != nil {
return nil, err
}

serial, err := depot.Serial()
if err != nil {
return nil, err
}

// create cert template
tmpl := &x509.Certificate{
SerialNumber: serial,
Subject: csr.Subject,
NotBefore: time.Now().Add(-600).UTC(),
NotAfter: time.Now().AddDate(0, 0, clientValidity).UTC(),
SubjectKeyId: id,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
},
SignatureAlgorithm: csr.SignatureAlgorithm,
EmailAddresses: csr.EmailAddresses,
}

crts, key, err := depot.CA([]byte(caPass))
ca := crts[0]
// sign the CSR creating a DER encoded cert
crtBytes, err := x509.CreateCertificate(rand.Reader, tmpl, ca, m.CSR.PublicKey, key)
if err != nil {
return nil, err
}
// parse the certificate
crt, err := x509.ParseCertificate(crtBytes)
if err != nil {
return nil, err
}

name := certName(crt)

// Test if this certificate is already in the CADB, revoke if needed
// revocation is done if the validity of the existing certificate is
// less than allowRenewal (14 days by default)
_, err = depot.HasCN(name, allowRenewal, crt, false)
if err != nil {
return nil, err
}

if err := depot.Put(name, crt); err != nil {
return nil, err
}

return crt, nil
}
}

func certName(crt *x509.Certificate) string {
if crt.Subject.CommonName != "" {
return crt.Subject.CommonName
}
return string(crt.Signature)
}

// rsaPublicKey reflects the ASN.1 structure of a PKCS#1 public key.
type rsaPublicKey struct {
N *big.Int
E int
}

// GenerateSubjectKeyID generates SubjectKeyId used in Certificate
// ID is 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
var pubBytes []byte
var err error
switch pub := pub.(type) {
case *rsa.PublicKey:
pubBytes, err = asn1.Marshal(rsaPublicKey{
N: pub.N,
E: pub.E,
})
if err != nil {
return nil, err
}
default:
return nil, errors.New("only RSA public key is supported")
}

hash := sha1.Sum(pubBytes)

return hash[:], nil
}
15 changes: 2 additions & 13 deletions scep/scep.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,25 +420,14 @@ func (msg *PKIMessage) Fail(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey,

}

// SignCSR creates an x509.Certificate based on a template and Cert Authority credentials
// returns a new PKIMessage with CertRep data
func (msg *PKIMessage) SignCSR(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, template *x509.Certificate) (*PKIMessage, error) {
// Success returns a new PKIMessage with CertRep data using an already-issued certificate
func (msg *PKIMessage) Success(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, crt *x509.Certificate) (*PKIMessage, error) {
// check if CSRReqMessage has already been decrypted
if msg.CSRReqMessage.CSR == nil {
if err := msg.DecryptPKIEnvelope(crtAuth, keyAuth); err != nil {
return nil, err
}
}
// sign the CSR creating a DER encoded cert
crtBytes, err := x509.CreateCertificate(rand.Reader, template, crtAuth, msg.CSRReqMessage.CSR.PublicKey, keyAuth)
if err != nil {
return nil, err
}
// parse the certificate
crt, err := x509.ParseCertificate(crtBytes)
if err != nil {
return nil, err
}

// create a degenerate cert structure
deg, err := DegenerateCertificates([]*x509.Certificate{crt})
Expand Down
Loading