pax_global_header00006660000000000000000000000064140723502220014507gustar00rootroot0000000000000052 comment=e25497e2faa8345b56d5ddfdbb174159ec745933 golang-github-micromdm-scep-2.0.0/000077500000000000000000000000001407235022200167725ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/.github/000077500000000000000000000000001407235022200203325ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/.github/workflows/000077500000000000000000000000001407235022200223675ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/.github/workflows/CI.yml000066400000000000000000000012761407235022200234130ustar00rootroot00000000000000name: CI on: push: branches: [ main ] pull_request: types: [opened, reopened, synchronize] jobs: build-test: name: Build, test & format strategy: matrix: go-version: [1.15.x, 1.16.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - name: setup go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Build run: go build -v ./... - name: Test run: go test -v ./... - name: Format if: matrix.platform == 'ubuntu-latest' run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi golang-github-micromdm-scep-2.0.0/.gitignore000066400000000000000000000000321407235022200207550ustar00rootroot00000000000000scepserver-* scepclient-* golang-github-micromdm-scep-2.0.0/Dockerfile000066400000000000000000000002331407235022200207620ustar00rootroot00000000000000FROM alpine:3 COPY ./scepclient-linux-amd64 /usr/bin/scepclient COPY ./scepserver-linux-amd64 /usr/bin/scepserver EXPOSE 8080 ENTRYPOINT ["scepserver"] golang-github-micromdm-scep-2.0.0/LICENSE000066400000000000000000000020611407235022200177760ustar00rootroot00000000000000MIT License Copyright (c) 2016 Victor Vrantchan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-micromdm-scep-2.0.0/Makefile000066400000000000000000000023261407235022200204350ustar00rootroot00000000000000VERSION=$(shell git describe --tags --always --dirty) LDFLAGS=-ldflags "-X main.version=$(VERSION)" OSARCH=$(shell go env GOHOSTOS)-$(shell go env GOHOSTARCH) SCEPCLIENT=\ scepclient-linux-amd64 \ scepclient-darwin-amd64 \ scepclient-freebsd-amd64 \ scepclient-windows-amd64.exe \ SCEPSERVER=\ scepserver-linux-amd64 \ scepserver-darwin-amd64 \ scepserver-freebsd-amd64 \ scepserver-windows-amd64.exe my: scepclient-$(OSARCH) scepserver-$(OSARCH) docker: scepclient-linux-amd64 scepserver-linux-amd64 $(SCEPCLIENT): GOOS=$(word 2,$(subst -, ,$@)) GOARCH=$(word 3,$(subst -, ,$(subst .exe,,$@))) go build $(LDFLAGS) -o $@ ./cmd/scepclient $(SCEPSERVER): GOOS=$(word 2,$(subst -, ,$@)) GOARCH=$(word 3,$(subst -, ,$(subst .exe,,$@))) go build $(LDFLAGS) -o $@ ./cmd/scepserver %-$(VERSION).zip: %.exe rm -f $@ zip $@ $< %-$(VERSION).zip: % rm -f $@ zip $@ $< release: $(foreach bin,$(SCEPCLIENT) $(SCEPSERVER),$(subst .exe,,$(bin))-$(VERSION).zip) clean: rm -f scepclient-* scepserver-* test: go test -cover ./... # don't run race tests by default. see https://github.com/etcd-io/bbolt/issues/187 test-race: go test -cover -race ./... .PHONY: my docker $(SCEPCLIENT) $(SCEPSERVER) release clean test test-race golang-github-micromdm-scep-2.0.0/README.md000066400000000000000000000161631407235022200202600ustar00rootroot00000000000000# scep [![CI](https://github.com/micromdm/scep/workflows/CI/badge.svg)](https://github.com/micromdm/scep/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/micromdm/scep/v2.svg)](https://pkg.go.dev/github.com/micromdm/scep/v2) `scep` is a Simple Certificate Enrollment Protocol server and client ## Installation Binary releases are available on the [releases page](https://github.com/micromdm/scep/releases). ### Compiling from source To compile the SCEP client and server you will need [a Go compiler](https://golang.org/dl/) as well as standard tools like git, make, etc. 1. Clone the repository and get into the source directory: `go get github.com/micromdm/scep && cd src/github.com/micromdm/scep` 2. Compile the client and server binaries: `make` The binaries will be compiled in the current directory and named after the architecture. I.e. `scepclient-linux-amd64` and `scepserver-linux-amd64`. ### Docker See Docker documentation below. ## Example setup Minimal example for both server and client. ``` # SERVER: # create a new CA ./scepserver-linux-amd64 ca -init # start server ./scepserver-linux-amd64 -depot depot -port 2016 -challenge=secret # SCEP request: # in a separate terminal window, run a client # note, if the client.key doesn't exist, the client will create a new rsa private key. Must be in PEM format. ./scepclient-linux-amd64 -private-key client.key -server-url=http://127.0.0.1:2016/scep -challenge=secret # NDES request: # note, this should point to an NDES server, scepserver does not provide NDES. ./scepclient-linux-amd64 -private-key client.key -server-url=https://scep.example.com:4321/certsrv/mscep/ -ca-fingerprint="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ``` ## Server Usage The default flags configure and run the scep server. `-depot` must be the path to a folder with `ca.pem` and `ca.key` files. If you don't already have a CA to use, you can create one using the `ca` subcommand. The scepserver provides one HTTP endpoint, `/scep`, that facilitates the normal PKIOperation/Message parameters. Server usage: ```sh $ ./scepserver-linux-amd64 -help -allowrenew string do not allow renewal until n days before expiry, set to 0 to always allow (default "14") -capass string passwd for the ca.key -challenge string enforce a challenge password -crtvalid string validity for new client certificates in days (default "365") -csrverifierexec string will be passed the CSRs for verification -debug enable debug logging -depot string path to ca folder (default "depot") -log-json output JSON logs -port string port to listen on (default "8080") -version prints version information usage: scep [] [] ca create/manage a CA type --help to see usage for each subcommand ``` Use the `ca -init` subcommand to create a new CA and private key. CA sub-command usage: ``` $ ./scepserver-linux-amd64 ca -help Usage of ca: -country string country for CA cert (default "US") -depot string path to ca folder (default "depot") -init create a new CA -key-password string password to store rsa key -keySize int rsa key size (default 4096) -organization string organization for CA cert (default "scep-ca") -organizational_unit string organizational unit (OU) for CA cert (default "SCEP CA") -years int default CA years (default 10) ``` ### CSR verifier The `-csrverifierexec` switch to the SCEP server allows for executing a command before a certificate is issued to verify the submitted CSR. Scripts exiting without errors (zero exit status) will proceed to certificate issuance, otherwise a SCEP error is generated to the client. For example if you wanted to just save the CSR this is a valid CSR verifier shell script: ```sh #!/bin/sh cat - > /tmp/scep.csr ``` ## Client Usage ```sh $ ./scepclient-linux-amd64 -help Usage of ./scepclient-linux-amd64: -ca-fingerprint string SHA-256 digest of CA certificate for NDES server. Note: Changed from MD5. -certificate string certificate path, if there is no key, scepclient will create one -challenge string enforce a challenge password -cn string common name for certificate (default "scepclient") -country string country code in certificate (default "US") -debug enable debug logging -keySize int rsa key size (default 2048) -locality string locality for certificate -log-json use JSON for log output -organization string organization for cert (default "scep-client") -ou string organizational unit for certificate (default "MDM") -private-key string private key path, if there is no key, scepclient will create one -province string province for certificate -server-url string SCEP server url -version prints version information ``` Note: Make sure to specify the desired endpoint in your `-server-url` value (e.g. `'http://scep.groob.io:2016/scep'`) To obtain a certificate through Network Device Enrollment Service (NDES), set `-server-url` to a server that provides NDES. This most likely uses the `/certsrv/mscep` path. You will need to add the `-ca-fingerprint` client argument during this request to specify which CA to use. If you're not sure which SHA-256 hash (for a specific CA) to use, you can use the `-debug` flag to print them out for the CAs returned from the SCEP server. ## Docker ```sh # first compile the Docker binaries make docker # build the image docker build -t micromdm/scep:latest . # create CA docker run -it --rm -v /path/to/ca/folder:/depot micromdm/scep:latest ca -init # run docker run -it --rm -v /path/to/ca/folder:/depot -p 8080:8080 micromdm/scep:latest ``` ## SCEP library The core `scep` library can be used for both client and server operations. ``` go get github.com/micromdm/scep/scep ``` For detailed usage, see the [Go Reference](https://pkg.go.dev/github.com/micromdm/scep/v2/scep). Example (server): ```go // read a request body containing SCEP message body, err := ioutil.ReadAll(r.Body) if err != nil { // handle err } // parse the SCEP message msg, err := scep.ParsePKIMessage(body) if err != nil { // handle err } // do something with msg fmt.Println(msg.MessageType) // extract encrypted pkiEnvelope err := msg.DecryptPKIEnvelope(CAcert, CAkey) if err != nil { // handle err } // use the CSR from decrypted PKCS request and sign // MyCSRSigner returns an *x509.Certificate here crt, err := MyCSRSigner(msg.CSRReqMessage.CSR) if err != nil { // handle err } // create a CertRep message from the original certRep, err := msg.Success(CAcert, CAkey, crt) if err != nil { // handle err } // send response back // w is a http.ResponseWriter w.Write(certRep.Raw) ``` ## Server library You can import the scep endpoint into another Go project. For an example take a look at [scepserver.go](cmd/scepserver/scepserver.go). The SCEP server includes a built-in CA/certificate store. This is facilitated by the `Depot` and `CSRSigner` Go interfaces. This certificate storage to happen however you want. It also allows for swapping out the entire CA signer altogether or even using SCEP as a proxy for certificates. golang-github-micromdm-scep-2.0.0/challenge/000077500000000000000000000000001407235022200207145ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/challenge/bolt/000077500000000000000000000000001407235022200216545ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/challenge/bolt/challenge.go000066400000000000000000000030161407235022200241250ustar00rootroot00000000000000package challengestore import ( "crypto/rand" "encoding/base64" "fmt" "github.com/boltdb/bolt" "github.com/pkg/errors" ) type Depot struct { *bolt.DB } const challengeBucket = "scep_challenges" // NewBoltDepot creates a depot.Depot backed by BoltDB. func NewBoltDepot(db *bolt.DB) (*Depot, error) { err := db.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(challengeBucket)) if err != nil { return fmt.Errorf("create bucket: %s", err) } return nil }) if err != nil { return nil, err } return &Depot{db}, nil } func (db *Depot) SCEPChallenge() (string, error) { key := make([]byte, 24) _, err := rand.Read(key) if err != nil { return "", err } challenge := base64.StdEncoding.EncodeToString(key) err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(challengeBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", challengeBucket) } return bucket.Put([]byte(challenge), []byte(challenge)) }) if err != nil { return "", err } return challenge, nil } func (db *Depot) HasChallenge(pw string) (bool, error) { tx, err := db.Begin(true) if err != nil { return false, errors.Wrap(err, "begin transaction") } bkt := tx.Bucket([]byte(challengeBucket)) if bkt == nil { return false, fmt.Errorf("bucket %q not found!", challengeBucket) } key := []byte(pw) var matches bool if chal := bkt.Get(key); chal != nil { if err := bkt.Delete(key); err != nil { return false, err } matches = true } return matches, tx.Commit() } golang-github-micromdm-scep-2.0.0/challenge/challenge.go000066400000000000000000000015111407235022200231630ustar00rootroot00000000000000// Package challenge defines an interface for a dynamic challenge password cache. package challenge import ( "crypto/x509" "errors" "github.com/micromdm/scep/v2/scep" scepserver "github.com/micromdm/scep/v2/server" ) // Store is a dynamic challenge password cache. type Store interface { SCEPChallenge() (string, error) HasChallenge(pw string) (bool, error) } // Middleware wraps next in a CSRSigner that verifies and invalidates the challenge func Middleware(store Store, next scepserver.CSRSigner) scepserver.CSRSignerFunc { return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { // TODO: compare challenge only for PKCSReq? valid, err := store.HasChallenge(m.ChallengePassword) if err != nil { return nil, err } if !valid { return nil, errors.New("invalid challenge") } return next.SignCSR(m) } } golang-github-micromdm-scep-2.0.0/challenge/challenge_bolt_test.go000066400000000000000000000033741407235022200252530ustar00rootroot00000000000000package challenge import ( "io/ioutil" "os" "testing" challengestore "github.com/micromdm/scep/v2/challenge/bolt" "github.com/micromdm/scep/v2/scep" scepserver "github.com/micromdm/scep/v2/server" "github.com/boltdb/bolt" ) 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 signer := Middleware(depot, scepserver.NopCSRSigner()) 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) } golang-github-micromdm-scep-2.0.0/client/000077500000000000000000000000001407235022200202505ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/client/client.go000066400000000000000000000012571407235022200220620ustar00rootroot00000000000000package scepclient import ( scepserver "github.com/micromdm/scep/v2/server" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" ) // Client is a SCEP Client type Client interface { scepserver.Service Supports(cap string) bool } // New creates a SCEP Client. func New( serverURL string, logger log.Logger, ) (Client, error) { endpoints, err := scepserver.MakeClientEndpoints(serverURL) if err != nil { return nil, err } logger = level.Info(logger) endpoints.GetEndpoint = scepserver.EndpointLoggingMiddleware(logger)(endpoints.GetEndpoint) endpoints.PostEndpoint = scepserver.EndpointLoggingMiddleware(logger)(endpoints.PostEndpoint) return endpoints, nil } golang-github-micromdm-scep-2.0.0/cmd/000077500000000000000000000000001407235022200175355ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/cmd/scepclient/000077500000000000000000000000001407235022200216665ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/cmd/scepclient/cert.go000066400000000000000000000044361407235022200231610ustar00rootroot00000000000000package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "io/ioutil" "math/big" "os" "time" ) const ( certificatePEMBlockType = "CERTIFICATE" ) func pemCert(derBytes []byte) []byte { pemBlock := &pem.Block{ Type: certificatePEMBlockType, Headers: nil, Bytes: derBytes, } out := pem.EncodeToMemory(pemBlock) return out } func loadOrSign(path string, priv *rsa.PrivateKey, csr *x509.CertificateRequest) (*x509.Certificate, error) { file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) if err != nil { if os.IsExist(err) { return loadPEMCertFromFile(path) } return nil, err } defer file.Close() self, err := selfSign(priv, csr) if err != nil { return nil, err } pemBlock := &pem.Block{ Type: certificatePEMBlockType, Headers: nil, Bytes: self.Raw, } if err = pem.Encode(file, pemBlock); err != nil { return nil, err } return self, nil } func selfSign(priv *rsa.PrivateKey, csr *x509.CertificateRequest) (*x509.Certificate, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, fmt.Errorf("failed to generate serial number: %s", err) } notBefore := time.Now() notAfter := notBefore.Add(time.Hour * 1) template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: "SCEP SIGNER", Organization: csr.Subject.Organization, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return nil, err } return x509.ParseCertificate(derBytes) } func loadPEMCertFromFile(path string) (*x509.Certificate, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode failed") } if pemBlock.Type != certificatePEMBlockType { return nil, errors.New("unmatched type or headers") } return x509.ParseCertificate(pemBlock.Bytes) } golang-github-micromdm-scep-2.0.0/cmd/scepclient/csr.go000066400000000000000000000044421407235022200230100ustar00rootroot00000000000000package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "io/ioutil" "os" "github.com/micromdm/scep/v2/cryptoutil/x509util" ) const ( csrPEMBlockType = "CERTIFICATE REQUEST" ) type csrOptions struct { cn, org, country, ou, locality, province, challenge string key *rsa.PrivateKey } func loadOrMakeCSR(path string, opts *csrOptions) (*x509.CertificateRequest, error) { file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) if err != nil { if os.IsExist(err) { return loadCSRfromFile(path) } return nil, err } defer file.Close() subject := pkix.Name{ CommonName: opts.cn, Organization: subjOrNil(opts.org), OrganizationalUnit: subjOrNil(opts.ou), Province: subjOrNil(opts.province), Locality: subjOrNil(opts.locality), Country: subjOrNil(opts.country), } template := x509util.CertificateRequest{ CertificateRequest: x509.CertificateRequest{ Subject: subject, SignatureAlgorithm: x509.SHA256WithRSA, }, } if opts.challenge != "" { template.ChallengePassword = opts.challenge } derBytes, err := x509util.CreateCertificateRequest(rand.Reader, &template, opts.key) pemBlock := &pem.Block{ Type: csrPEMBlockType, Bytes: derBytes, } if err := pem.Encode(file, pemBlock); err != nil { return nil, err } return x509.ParseCertificateRequest(derBytes) } // returns nil or []string{input} to populate pkix.Name.Subject func subjOrNil(input string) []string { if input == "" { return nil } return []string{input} } // convert DER to PEM format func pemCSR(derBytes []byte) []byte { pemBlock := &pem.Block{ Type: csrPEMBlockType, Headers: nil, Bytes: derBytes, } return pem.EncodeToMemory(pemBlock) } // load PEM encoded CSR from file func loadCSRfromFile(path string) (*x509.CertificateRequest, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("cannot find the next PEM formatted block") } if pemBlock.Type != csrPEMBlockType || len(pemBlock.Headers) != 0 { return nil, errors.New("unmatched type or headers") } return x509.ParseCertificateRequest(pemBlock.Bytes) } golang-github-micromdm-scep-2.0.0/cmd/scepclient/key.go000066400000000000000000000026421407235022200230110ustar00rootroot00000000000000package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "io/ioutil" "os" ) const ( rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" ) // create a new RSA private key func newRSAKey(bits int) (*rsa.PrivateKey, error) { private, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { return nil, err } return private, nil } // load key if it exists or create a new one func loadOrMakeKey(path string, rsaBits int) (*rsa.PrivateKey, error) { file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) if err != nil { if os.IsExist(err) { return loadKeyFromFile(path) } return nil, err } defer file.Close() // write key priv, err := newRSAKey(rsaBits) if err != nil { return nil, err } privBytes := x509.MarshalPKCS1PrivateKey(priv) pemBlock := &pem.Block{ Type: rsaPrivateKeyPEMBlockType, Headers: nil, Bytes: privBytes, } if err = pem.Encode(file, pemBlock); err != nil { return nil, err } return priv, nil } // load a PEM private key from disk func loadKeyFromFile(path string) (*rsa.PrivateKey, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode failed") } if pemBlock.Type != rsaPrivateKeyPEMBlockType { return nil, errors.New("unmatched type or headers") } return x509.ParsePKCS1PrivateKey(pemBlock.Bytes) } golang-github-micromdm-scep-2.0.0/cmd/scepclient/scepclient.go000066400000000000000000000215641407235022200243560ustar00rootroot00000000000000package main import ( "context" "crypto/sha256" "crypto/x509" "encoding/hex" "flag" "fmt" "io/ioutil" stdlog "log" "net/url" "os" "path/filepath" "strings" "time" scepclient "github.com/micromdm/scep/v2/client" "github.com/micromdm/scep/v2/scep" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" ) // version info var ( version = "unknown" ) type runCfg struct { dir string csrPath string keyPath string keyBits int selfSignPath string certPath string cn string org string ou string locality string province string country string challenge string serverURL string caCertsSelector scep.CertsSelector debug bool logfmt string caCertMsg string } func run(cfg runCfg) error { ctx := context.Background() var logger log.Logger { if strings.ToLower(cfg.logfmt) == "json" { logger = log.NewJSONLogger(os.Stderr) } else { logger = log.NewLogfmtLogger(os.Stderr) } stdlog.SetOutput(log.NewStdlibAdapter(logger)) logger = log.With(logger, "ts", log.DefaultTimestampUTC) if !cfg.debug { logger = level.NewFilter(logger, level.AllowInfo()) } } lginfo := level.Info(logger) client, err := scepclient.New(cfg.serverURL, logger) if err != nil { return err } key, err := loadOrMakeKey(cfg.keyPath, cfg.keyBits) if err != nil { return err } opts := &csrOptions{ cn: cfg.cn, org: cfg.org, country: strings.ToUpper(cfg.country), ou: cfg.ou, locality: cfg.locality, province: cfg.province, challenge: cfg.challenge, key: key, } csr, err := loadOrMakeCSR(cfg.csrPath, opts) if err != nil { fmt.Println(err) os.Exit(1) } var self *x509.Certificate cert, err := loadPEMCertFromFile(cfg.certPath) if err != nil { if !os.IsNotExist(err) { return err } s, err := loadOrSign(cfg.selfSignPath, key, csr) if err != nil { return err } self = s } resp, certNum, err := client.GetCACert(ctx, cfg.caCertMsg) if err != nil { return err } var certs []*x509.Certificate { if certNum > 1 { certs, err = scep.CACerts(resp) if err != nil { return err } } else { certs, err = x509.ParseCertificates(resp) if err != nil { return err } } } if cfg.debug { logCerts(level.Debug(logger), certs) } var signerCert *x509.Certificate { if cert != nil { signerCert = cert } else { signerCert = self } } var msgType scep.MessageType { // TODO validate CA and set UpdateReq if needed if cert != nil { msgType = scep.RenewalReq } else { msgType = scep.PKCSReq } } tmpl := &scep.PKIMessage{ MessageType: msgType, Recipients: certs, SignerKey: key, SignerCert: signerCert, } if cfg.challenge != "" && msgType == scep.PKCSReq { tmpl.CSRReqMessage = &scep.CSRReqMessage{ ChallengePassword: cfg.challenge, } } msg, err := scep.NewCSRRequest(csr, tmpl, scep.WithLogger(logger), scep.WithCertsSelector(cfg.caCertsSelector)) if err != nil { return errors.Wrap(err, "creating csr pkiMessage") } var respMsg *scep.PKIMessage for { // loop in case we get a PENDING response which requires // a manual approval. respBytes, err := client.PKIOperation(ctx, msg.Raw) if err != nil { return errors.Wrapf(err, "PKIOperation for %s", msgType) } respMsg, err = scep.ParsePKIMessage(respBytes, scep.WithLogger(logger), scep.WithCACerts(msg.Recipients)) if err != nil { return errors.Wrapf(err, "parsing pkiMessage response %s", msgType) } switch respMsg.PKIStatus { case scep.FAILURE: return errors.Errorf("%s request failed, failInfo: %s", msgType, respMsg.FailInfo) case scep.PENDING: lginfo.Log("pkiStatus", "PENDING", "msg", "sleeping for 30 seconds, then trying again.") time.Sleep(30 * time.Second) continue } lginfo.Log("pkiStatus", "SUCCESS", "msg", "server returned a certificate.") break // on scep.SUCCESS } if err := respMsg.DecryptPKIEnvelope(signerCert, key); err != nil { return errors.Wrapf(err, "decrypt pkiEnvelope, msgType: %s, status %s", msgType, respMsg.PKIStatus) } respCert := respMsg.CertRepMessage.Certificate if err := ioutil.WriteFile(cfg.certPath, pemCert(respCert.Raw), 0666); err != nil { return err } // remove self signer if used if self != nil { if err := os.Remove(cfg.selfSignPath); err != nil { return err } } return nil } // logCerts logs the count, number, RDN, and SHA-256 of certs to logger func logCerts(logger log.Logger, certs []*x509.Certificate) { logger.Log("msg", "cacertlist", "count", len(certs)) for i, cert := range certs { logger.Log( "msg", "cacertlist", "number", i, "rdn", cert.Subject.ToRDNSequence().String(), "sha256", fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), ) } } // validateSHA256Fingerprint makes sure fingerprint looks like a SHA-256 hash. // We remove spaces and colons from fingerprint as it may come in various forms: // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 // E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 // e3b0c442 98fc1c14 9afbf4c8 996fb924 27ae41e4 649b934c a495991b 7852b855 // e3:b0:c4:42:98:fc:1c:14:9a:fb:f4:c8:99:6f:b9:24:27:ae:41:e4:64:9b:93:4c:a4:95:99:1b:78:52:b8:55 func validateSHA256Fingerprint(fingerprint string) (hash [32]byte, err error) { fingerprint = strings.NewReplacer(" ", "", ":", "").Replace(fingerprint) byteSlice, err := hex.DecodeString(fingerprint) copy(hash[:], byteSlice) if err != nil { return } // check for length of SHA-256 if len(byteSlice) != 32 { err = errors.New("invalid SHA-256 hash length") } return } func validateFlags(keyPath, serverURL string) error { if keyPath == "" { return errors.New("must specify private key path") } if serverURL == "" { return errors.New("must specify server-url flag parameter") } _, err := url.Parse(serverURL) if err != nil { return fmt.Errorf("invalid server-url flag parameter %s", err) } return nil } func main() { var ( flVersion = flag.Bool("version", false, "prints version information") flServerURL = flag.String("server-url", "", "SCEP server url") flChallengePassword = flag.String("challenge", "", "enforce a challenge password") flPKeyPath = flag.String("private-key", "", "private key path, if there is no key, scepclient will create one") flCertPath = flag.String("certificate", "", "certificate path, if there is no key, scepclient will create one") flKeySize = flag.Int("keySize", 2048, "rsa key size") flOrg = flag.String("organization", "scep-client", "organization for cert") flCName = flag.String("cn", "scepclient", "common name for certificate") flOU = flag.String("ou", "MDM", "organizational unit for certificate") flLoc = flag.String("locality", "", "locality for certificate") flProvince = flag.String("province", "", "province for certificate") flCountry = flag.String("country", "US", "country code in certificate") flCACertMessage = flag.String("cacert-message", "", "message sent with GetCACert operation") // in case of multiple certificate authorities, we need to figure out who the recipient of the encrypted // data is. flCAFingerprint = flag.String("ca-fingerprint", "", "SHA-256 digest of CA certificate for NDES server. Note: Changed from MD5.") flDebugLogging = flag.Bool("debug", false, "enable debug logging") flLogJSON = flag.Bool("log-json", false, "use JSON for log output") ) flag.Parse() // print version information if *flVersion { fmt.Println(version) os.Exit(0) } if err := validateFlags(*flPKeyPath, *flServerURL); err != nil { fmt.Println(err) os.Exit(1) } caCertsSelector := scep.NopCertsSelector() if *flCAFingerprint != "" { hash, err := validateSHA256Fingerprint(*flCAFingerprint) if err != nil { fmt.Println(fmt.Errorf("invalid fingerprint: %v", err)) os.Exit(1) } caCertsSelector = scep.SHA256FingerprintCertsSelector(hash) } dir := filepath.Dir(*flPKeyPath) csrPath := dir + "/csr.pem" selfSignPath := dir + "/self.pem" if *flCertPath == "" { *flCertPath = dir + "/client.pem" } var logfmt string if *flLogJSON { logfmt = "json" } cfg := runCfg{ dir: dir, csrPath: csrPath, keyPath: *flPKeyPath, keyBits: *flKeySize, selfSignPath: selfSignPath, certPath: *flCertPath, cn: *flCName, org: *flOrg, country: *flCountry, locality: *flLoc, ou: *flOU, province: *flProvince, challenge: *flChallengePassword, serverURL: *flServerURL, caCertsSelector: caCertsSelector, debug: *flDebugLogging, logfmt: logfmt, caCertMsg: *flCACertMessage, } if err := run(cfg); err != nil { fmt.Println(err) os.Exit(1) } } golang-github-micromdm-scep-2.0.0/cmd/scepserver/000077500000000000000000000000001407235022200217165ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/cmd/scepserver/scepserver.go000066400000000000000000000225361407235022200244360ustar00rootroot00000000000000package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "flag" "fmt" "math/big" "net/http" "os" "os/signal" "path/filepath" "strconv" "syscall" "time" "github.com/micromdm/scep/v2/cryptoutil" "github.com/micromdm/scep/v2/csrverifier" executablecsrverifier "github.com/micromdm/scep/v2/csrverifier/executable" scepdepot "github.com/micromdm/scep/v2/depot" "github.com/micromdm/scep/v2/depot/file" scepserver "github.com/micromdm/scep/v2/server" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" ) // version info var ( version = "unknown" ) func main() { var caCMD = flag.NewFlagSet("ca", flag.ExitOnError) { if len(os.Args) >= 2 { if os.Args[1] == "ca" { status := caMain(caCMD) os.Exit(status) } } } //main flags var ( flVersion = flag.Bool("version", false, "prints version information") flPort = flag.String("port", envString("SCEP_HTTP_LISTEN_PORT", "8080"), "port to listen on") flDepotPath = flag.String("depot", envString("SCEP_FILE_DEPOT", "depot"), "path to ca folder") flCAPass = flag.String("capass", envString("SCEP_CA_PASS", ""), "passwd for the ca.key") flClDuration = flag.String("crtvalid", envString("SCEP_CERT_VALID", "365"), "validity for new client certificates in days") flClAllowRenewal = flag.String("allowrenew", envString("SCEP_CERT_RENEW", "14"), "do not allow renewal until n days before expiry, set to 0 to always allow") flChallengePassword = flag.String("challenge", envString("SCEP_CHALLENGE_PASSWORD", ""), "enforce a challenge password") flCSRVerifierExec = flag.String("csrverifierexec", envString("SCEP_CSR_VERIFIER_EXEC", ""), "will be passed the CSRs for verification") flDebug = flag.Bool("debug", envBool("SCEP_LOG_DEBUG"), "enable debug logging") flLogJSON = flag.Bool("log-json", envBool("SCEP_LOG_JSON"), "output JSON logs") ) flag.Usage = func() { flag.PrintDefaults() fmt.Println("usage: scep [] []") fmt.Println(" ca create/manage a CA") fmt.Println("type --help to see usage for each subcommand") } flag.Parse() // print version information if *flVersion { fmt.Println(version) os.Exit(0) } port := ":" + *flPort var logger log.Logger { if *flLogJSON { logger = log.NewJSONLogger(os.Stderr) } else { logger = log.NewLogfmtLogger(os.Stderr) } if !*flDebug { logger = level.NewFilter(logger, level.AllowInfo()) } logger = log.With(logger, "ts", log.DefaultTimestampUTC) logger = log.With(logger, "caller", log.DefaultCaller) } lginfo := level.Info(logger) var err error var depot scepdepot.Depot // cert storage { depot, err = file.NewFileDepot(*flDepotPath) if err != nil { lginfo.Log("err", err) os.Exit(1) } } allowRenewal, err := strconv.Atoi(*flClAllowRenewal) if err != nil { lginfo.Log("err", err, "msg", "No valid number for allowed renewal time") os.Exit(1) } clientValidity, err := strconv.Atoi(*flClDuration) if err != nil { lginfo.Log("err", err, "msg", "No valid number for client cert validity") os.Exit(1) } var csrVerifier csrverifier.CSRVerifier if *flCSRVerifierExec > "" { executableCSRVerifier, err := executablecsrverifier.New(*flCSRVerifierExec, lginfo) if err != nil { lginfo.Log("err", err, "msg", "Could not instantiate CSR verifier") os.Exit(1) } csrVerifier = executableCSRVerifier } var svc scepserver.Service // scep service { 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) } var signer scepserver.CSRSigner = scepdepot.NewSigner( depot, scepdepot.WithAllowRenewalDays(allowRenewal), scepdepot.WithValidityDays(clientValidity), scepdepot.WithCAPass(*flCAPass), ) if *flChallengePassword != "" { signer = scepserver.ChallengeMiddleware(*flChallengePassword, signer) } if csrVerifier != nil { signer = csrverifier.Middleware(csrVerifier, signer) } svc, err = scepserver.NewService(crts[0], key, signer, scepserver.WithLogger(logger)) if err != nil { lginfo.Log("err", err) os.Exit(1) } svc = scepserver.NewLoggingService(log.With(lginfo, "component", "scep_service"), svc) } var h http.Handler // http handler { e := scepserver.MakeServerEndpoints(svc) e.GetEndpoint = scepserver.EndpointLoggingMiddleware(lginfo)(e.GetEndpoint) e.PostEndpoint = scepserver.EndpointLoggingMiddleware(lginfo)(e.PostEndpoint) h = scepserver.MakeHTTPHandler(e, svc, log.With(lginfo, "component", "http")) } // start http server errs := make(chan error, 2) go func() { lginfo.Log("transport", "http", "address", port, "msg", "listening") errs <- http.ListenAndServe(port, h) }() go func() { c := make(chan os.Signal) signal.Notify(c, syscall.SIGINT) errs <- fmt.Errorf("%s", <-c) }() lginfo.Log("terminated", <-errs) } func caMain(cmd *flag.FlagSet) int { var ( flDepotPath = cmd.String("depot", "depot", "path to ca folder") flInit = cmd.Bool("init", false, "create a new CA") flYears = cmd.Int("years", 10, "default CA years") flKeySize = cmd.Int("keySize", 4096, "rsa key size") flOrg = cmd.String("organization", "scep-ca", "organization for CA cert") flOrgUnit = cmd.String("organizational_unit", "SCEP CA", "organizational unit (OU) for CA cert") flPassword = cmd.String("key-password", "", "password to store rsa key") flCountry = cmd.String("country", "US", "country for CA cert") ) cmd.Parse(os.Args[2:]) if *flInit { fmt.Println("Initializing new CA") key, err := createKey(*flKeySize, []byte(*flPassword), *flDepotPath) if err != nil { fmt.Println(err) return 1 } if err := createCertificateAuthority(key, *flYears, *flOrg, *flOrgUnit, *flCountry, *flDepotPath); err != nil { fmt.Println(err) return 1 } } return 0 } // create a key, save it to depot and return it for further usage. func createKey(bits int, password []byte, depot string) (*rsa.PrivateKey, error) { // create depot folder if missing if err := os.MkdirAll(depot, 0755); err != nil { return nil, err } name := filepath.Join(depot, "ca.key") file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) if err != nil { return nil, err } defer file.Close() // create RSA key and save as PEM file key, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { return nil, err } privPEMBlock, err := x509.EncryptPEMBlock( rand.Reader, rsaPrivateKeyPEMBlockType, x509.MarshalPKCS1PrivateKey(key), password, x509.PEMCipher3DES, ) if err != nil { return nil, err } if err := pem.Encode(file, privPEMBlock); err != nil { os.Remove(name) return nil, err } return key, nil } func createCertificateAuthority(key *rsa.PrivateKey, years int, organization string, organizationalUnit string, country string, depot string) error { var ( authPkixName = pkix.Name{ Country: nil, Organization: nil, OrganizationalUnit: nil, Locality: nil, Province: nil, StreetAddress: nil, PostalCode: nil, SerialNumber: "", CommonName: "", } // Build CA based on RFC5280 authTemplate = x509.Certificate{ SerialNumber: big.NewInt(1), Subject: authPkixName, // NotBefore is set to be 10min earlier to fix gap on time difference in cluster NotBefore: time.Now().Add(-600).UTC(), NotAfter: time.Time{}, // Used for certificate signing only KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, ExtKeyUsage: nil, UnknownExtKeyUsage: nil, // activate CA BasicConstraintsValid: true, IsCA: true, // Not allow any non-self-issued intermediate CA MaxPathLen: 0, // 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey // (excluding the tag, length, and number of unused bits) // **SHOULD** be filled in later SubjectKeyId: nil, // Subject Alternative Name DNSNames: nil, PermittedDNSDomainsCritical: false, PermittedDNSDomains: nil, } ) subjectKeyID, err := cryptoutil.GenerateSubjectKeyID(&key.PublicKey) if err != nil { return err } authTemplate.SubjectKeyId = subjectKeyID authTemplate.NotAfter = time.Now().AddDate(years, 0, 0).UTC() authTemplate.Subject.Country = []string{country} authTemplate.Subject.Organization = []string{organization} authTemplate.Subject.OrganizationalUnit = []string{organizationalUnit} crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, &key.PublicKey, key) if err != nil { return err } name := filepath.Join(depot, "ca.pem") file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) if err != nil { return err } defer file.Close() if _, err := file.Write(pemCert(crtBytes)); err != nil { file.Close() os.Remove(name) return err } return nil } const ( rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" certificatePEMBlockType = "CERTIFICATE" ) func pemCert(derBytes []byte) []byte { pemBlock := &pem.Block{ Type: certificatePEMBlockType, Headers: nil, Bytes: derBytes, } out := pem.EncodeToMemory(pemBlock) return out } func envString(key, def string) string { if env := os.Getenv(key); env != "" { return env } return def } func envBool(key string) bool { if env := os.Getenv(key); env == "true" { return true } return false } golang-github-micromdm-scep-2.0.0/cryptoutil/000077500000000000000000000000001407235022200212105ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/cryptoutil/cryptoutil.go000066400000000000000000000017021407235022200237550ustar00rootroot00000000000000package cryptoutil import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "crypto/sha256" "encoding/asn1" "github.com/pkg/errors" ) // GenerateSubjectKeyID generates Subject Key Identifier (SKI) using SHA-256 // hash of the public key bytes according to RFC 7093 section 2. func GenerateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { var pubBytes []byte var err error switch pub := pub.(type) { case *rsa.PublicKey: pubBytes, err = asn1.Marshal(*pub) if err != nil { return nil, err } case *ecdsa.PublicKey: pubBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) default: return nil, errors.New("only ECDSA and RSA public keys are supported") } hash := sha256.Sum256(pubBytes) // According to RFC 7093, The keyIdentifier is composed of the leftmost // 160-bits of the SHA-256 hash of the value of the BIT STRING // subjectPublicKey (excluding the tag, length, and number of unused bits). return hash[:20], nil } golang-github-micromdm-scep-2.0.0/cryptoutil/cryptoutil_test.go000066400000000000000000000020041407235022200250100ustar00rootroot00000000000000package cryptoutil import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "math/big" "testing" ) func TestGenerateSubjectKeyID(t *testing.T) { for _, test := range []struct { testName string pub crypto.PublicKey }{ {"RSA", &rsa.PublicKey{N: big.NewInt(123), E: 65537}}, {"ECDSA", &ecdsa.PublicKey{X: big.NewInt(123), Y: big.NewInt(123), Curve: elliptic.P224()}}, } { test := test t.Run(test.testName, func(t *testing.T) { t.Parallel() ski, err := GenerateSubjectKeyID(test.pub) if err != nil { t.Fatal(err) } if len(ski) != 20 { t.Fatalf("unexpected subject public key identifier length: %d", len(ski)) } ski2, err := GenerateSubjectKeyID(test.pub) if err != nil { t.Fatal(err) } if !testSKIEq(ski, ski2) { t.Fatal("subject key identifier generation is not deterministic") } }) } } func testSKIEq(a, b []byte) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } golang-github-micromdm-scep-2.0.0/cryptoutil/doc.go000066400000000000000000000001331407235022200223010ustar00rootroot00000000000000// package cryptoutil provides utilities for working with crypto types. package cryptoutil golang-github-micromdm-scep-2.0.0/cryptoutil/x509util/000077500000000000000000000000001407235022200226135ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/cryptoutil/x509util/doc.go000066400000000000000000000001211407235022200237010ustar00rootroot00000000000000// package x509 provides utilities for working with x509 types. package x509util golang-github-micromdm-scep-2.0.0/cryptoutil/x509util/x509util.go000066400000000000000000000270651407235022200245570ustar00rootroot00000000000000/* Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package x509util import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" _ "crypto/sha256" _ "crypto/sha512" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "errors" "io" ) type CertificateRequest struct { x509.CertificateRequest ChallengePassword string } // CreateCertificateRequest creates a new certificate request based on a template. // The resulting CSR is similar to x509 but optionally supports the // challengePassword attribute. // // See https://github.com/golang/go/issues/15995 func CreateCertificateRequest(rand io.Reader, template *CertificateRequest, priv interface{}) (csr []byte, err error) { if template.ChallengePassword == "" { // if no challenge password, return a stdlib CSR. return x509.CreateCertificateRequest(rand, &template.CertificateRequest, priv) } derBytes, err := x509.CreateCertificateRequest(rand, &template.CertificateRequest, priv) if err != nil { return nil, err } // add the challenge attribute to the CSR, then re-sign the raw csr. // not checking the crypto.Signer assertion because x509.CreateCertificateRequest already did that. return addChallenge( template.CertificateRequest.SignatureAlgorithm, rand, derBytes, template.ChallengePassword, priv.(crypto.Signer), ) } type passwordChallengeAttribute struct { Type asn1.ObjectIdentifier Value []string `asn1:"set"` } // The structures below are copied from the Go standard library x509 package. type publicKeyInfo struct { Raw asn1.RawContent Algorithm pkix.AlgorithmIdentifier PublicKey asn1.BitString } type tbsCertificateRequest struct { Raw asn1.RawContent Version int Subject asn1.RawValue PublicKey publicKeyInfo RawAttributes []asn1.RawValue `asn1:"tag:0"` } type certificateRequest struct { Raw asn1.RawContent TBSCSR tbsCertificateRequest SignatureAlgorithm pkix.AlgorithmIdentifier SignatureValue asn1.BitString } // ParseChallengePassword extracts the challengePassword attribute from a // DER encoded Certificate Signing Request. func ParseChallengePassword(asn1Data []byte) (string, error) { type attribute struct { ID asn1.ObjectIdentifier Value asn1.RawValue `asn1:"set"` } var csr certificateRequest rest, err := asn1.Unmarshal(asn1Data, &csr) if err != nil { return "", err } else if len(rest) != 0 { err = asn1.SyntaxError{Msg: "trailing data"} return "", err } var password string for _, rawAttr := range csr.TBSCSR.RawAttributes { var attr attribute _, err := asn1.Unmarshal(rawAttr.FullBytes, &attr) if err != nil { return "", err } if attr.ID.Equal(oidChallengePassword) { _, err := asn1.Unmarshal(attr.Value.Bytes, &password) if err != nil { return "", err } } } return password, nil } // addChallenge takes a raw CSR created by x509.CreateCertificateRequest, // adds a passwordChallengeAttribute and re-signs the raw CSR bytes. func addChallenge( templateSigAlgo x509.SignatureAlgorithm, reader io.Reader, derBytes []byte, challenge string, key crypto.Signer, ) (csr []byte, err error) { var hashFunc crypto.Hash var sigAlgo pkix.AlgorithmIdentifier hashFunc, sigAlgo, err = signingParamsForPublicKey(key.Public(), templateSigAlgo) if err != nil { return nil, err } var req certificateRequest rest, err := asn1.Unmarshal(derBytes, &req) if err != nil { return nil, err } else if len(rest) != 0 { err = asn1.SyntaxError{Msg: "trailing data"} return nil, err } passwordAttribute := passwordChallengeAttribute{ Type: oidChallengePassword, Value: []string{challenge}, } b, err := asn1.Marshal(passwordAttribute) if err != nil { return nil, err } var rawAttribute asn1.RawValue rest, err = asn1.Unmarshal(b, &rawAttribute) if err != nil { return nil, err } else if len(rest) != 0 { err = asn1.SyntaxError{Msg: "trailing data"} return nil, err } // append attribute req.TBSCSR.RawAttributes = append(req.TBSCSR.RawAttributes, rawAttribute) // recreate request tbsCSR := tbsCertificateRequest{ Version: 0, Subject: req.TBSCSR.Subject, PublicKey: req.TBSCSR.PublicKey, RawAttributes: req.TBSCSR.RawAttributes, } tbsCSRContents, err := asn1.Marshal(tbsCSR) if err != nil { return nil, err } tbsCSR.Raw = tbsCSRContents h := hashFunc.New() if _, err := h.Write(tbsCSRContents); err != nil { return nil, err } var signature []byte signature, err = key.Sign(reader, h.Sum(nil), hashFunc) if err != nil { return nil, err } return asn1.Marshal(certificateRequest{ TBSCSR: tbsCSR, SignatureAlgorithm: sigAlgo, SignatureValue: asn1.BitString{ Bytes: signature, BitLength: len(signature) * 8, }, }) } // signingParamsForPublicKey returns the parameters to use for signing with // priv. If requestedSigAlgo is not zero then it overrides the default // signature algorithm. func signingParamsForPublicKey(pub interface{}, requestedSigAlgo x509.SignatureAlgorithm) (hashFunc crypto.Hash, sigAlgo pkix.AlgorithmIdentifier, err error) { var pubType x509.PublicKeyAlgorithm switch pub := pub.(type) { case *rsa.PublicKey: pubType = x509.RSA hashFunc = crypto.SHA256 sigAlgo.Algorithm = oidSignatureSHA256WithRSA sigAlgo.Parameters = asn1NullRawValue case *ecdsa.PublicKey: pubType = x509.ECDSA switch pub.Curve { case elliptic.P224(), elliptic.P256(): hashFunc = crypto.SHA256 sigAlgo.Algorithm = oidSignatureECDSAWithSHA256 case elliptic.P384(): hashFunc = crypto.SHA384 sigAlgo.Algorithm = oidSignatureECDSAWithSHA384 case elliptic.P521(): hashFunc = crypto.SHA512 sigAlgo.Algorithm = oidSignatureECDSAWithSHA512 default: err = errors.New("x509: unknown elliptic curve") } default: err = errors.New("x509: only RSA and ECDSA keys supported") } if err != nil { return } if requestedSigAlgo == 0 { return } found := false for _, details := range signatureAlgorithmDetails { if details.algo == requestedSigAlgo { if details.pubKeyAlgo != pubType { err = errors.New("x509: requested SignatureAlgorithm does not match private key type") return } sigAlgo.Algorithm, hashFunc = details.oid, details.hash if hashFunc == 0 { err = errors.New("x509: cannot sign with hash function requested") return } // copy x509.SignatureAlgorithm.isRSAPSS method isRSAPSS := func() bool { switch requestedSigAlgo { case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: return true default: return false } } if isRSAPSS() { sigAlgo.Parameters = rsaPSSParameters(hashFunc) } found = true break } } if !found { err = errors.New("x509: unknown SignatureAlgorithm") } return } var signatureAlgorithmDetails = []struct { algo x509.SignatureAlgorithm oid asn1.ObjectIdentifier pubKeyAlgo x509.PublicKeyAlgorithm hash crypto.Hash }{ {x509.SHA256WithRSA, oidSignatureSHA256WithRSA, x509.RSA, crypto.SHA256}, {x509.SHA384WithRSA, oidSignatureSHA384WithRSA, x509.RSA, crypto.SHA384}, {x509.SHA512WithRSA, oidSignatureSHA512WithRSA, x509.RSA, crypto.SHA512}, {x509.SHA256WithRSAPSS, oidSignatureRSAPSS, x509.RSA, crypto.SHA256}, {x509.SHA384WithRSAPSS, oidSignatureRSAPSS, x509.RSA, crypto.SHA384}, {x509.SHA512WithRSAPSS, oidSignatureRSAPSS, x509.RSA, crypto.SHA512}, {x509.DSAWithSHA256, oidSignatureDSAWithSHA256, x509.DSA, crypto.SHA256}, {x509.ECDSAWithSHA256, oidSignatureECDSAWithSHA256, x509.ECDSA, crypto.SHA256}, {x509.ECDSAWithSHA384, oidSignatureECDSAWithSHA384, x509.ECDSA, crypto.SHA384}, {x509.ECDSAWithSHA512, oidSignatureECDSAWithSHA512, x509.ECDSA, crypto.SHA512}, } var ( oidSignatureSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} oidSignatureSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12} oidSignatureSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13} oidSignatureRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} oidSignatureDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2} oidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} oidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3} oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4} oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} oidSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} oidSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} oidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8} oidChallengePassword = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7} ) // added to Go in 1.9 var asn1NullRawValue = asn1.RawValue{ Tag: 5, /* ASN.1 NULL */ } // pssParameters reflects the parameters in an AlgorithmIdentifier that // specifies RSA PSS. See https://tools.ietf.org/html/rfc3447#appendix-A.2.3 type pssParameters struct { // The following three fields are not marked as // optional because the default values specify SHA-1, // which is no longer suitable for use in signatures. Hash pkix.AlgorithmIdentifier `asn1:"explicit,tag:0"` MGF pkix.AlgorithmIdentifier `asn1:"explicit,tag:1"` SaltLength int `asn1:"explicit,tag:2"` TrailerField int `asn1:"optional,explicit,tag:3,default:1"` } // rsaPSSParameters returns an asn1.RawValue suitable for use as the Parameters // in an AlgorithmIdentifier that specifies RSA PSS. func rsaPSSParameters(hashFunc crypto.Hash) asn1.RawValue { var hashOID asn1.ObjectIdentifier switch hashFunc { case crypto.SHA256: hashOID = oidSHA256 case crypto.SHA384: hashOID = oidSHA384 case crypto.SHA512: hashOID = oidSHA512 } params := pssParameters{ Hash: pkix.AlgorithmIdentifier{ Algorithm: hashOID, Parameters: asn1NullRawValue, }, MGF: pkix.AlgorithmIdentifier{ Algorithm: oidMGF1, }, SaltLength: hashFunc.Size(), TrailerField: 1, } mgf1Params := pkix.AlgorithmIdentifier{ Algorithm: hashOID, Parameters: asn1NullRawValue, } var err error params.MGF.Parameters.FullBytes, err = asn1.Marshal(mgf1Params) if err != nil { panic(err) } serialized, err := asn1.Marshal(params) if err != nil { panic(err) } return asn1.RawValue{FullBytes: serialized} } golang-github-micromdm-scep-2.0.0/cryptoutil/x509util/x509util_test.go000066400000000000000000000020601407235022200256020ustar00rootroot00000000000000package x509util import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "testing" ) func TestCreateCertificateRequest(t *testing.T) { r := rand.Reader priv, err := rsa.GenerateKey(r, 1024) if err != nil { t.Fatal(err) } template := CertificateRequest{ CertificateRequest: x509.CertificateRequest{ Subject: pkix.Name{ CommonName: "test.acme.co", Country: []string{"US"}, }, }, ChallengePassword: "foobar", } derBytes, err := CreateCertificateRequest(r, &template, priv) if err != nil { t.Fatal(err) } out, err := x509.ParseCertificateRequest(derBytes) if err != nil { t.Fatalf("failed to create certificate request: %s", err) } if err := out.CheckSignature(); err != nil { t.Errorf("failed to check certificate request signature: %s", err) } challenge, err := ParseChallengePassword(derBytes) if err != nil { t.Fatalf("failed to parse challengePassword attribute: %s", err) } if have, want := challenge, template.ChallengePassword; have != want { t.Errorf("have %s, want %s", have, want) } } golang-github-micromdm-scep-2.0.0/csrverifier/000077500000000000000000000000001407235022200213155ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/csrverifier/csrverifier.go000066400000000000000000000013161407235022200241700ustar00rootroot00000000000000// Package csrverifier defines an interface for CSR verification. package csrverifier import ( "crypto/x509" "errors" "github.com/micromdm/scep/v2/scep" scepserver "github.com/micromdm/scep/v2/server" ) // CSRVerifier verifies the raw decrypted CSR. type CSRVerifier interface { Verify(data []byte) (bool, error) } // Middleware wraps next in a CSRSigner that runs verifier func Middleware(verifier CSRVerifier, next scepserver.CSRSigner) scepserver.CSRSignerFunc { return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { ok, err := verifier.Verify(m.RawDecrypted) if err != nil { return nil, err } if !ok { return nil, errors.New("CSR verify failed") } return next.SignCSR(m) } } golang-github-micromdm-scep-2.0.0/csrverifier/executable/000077500000000000000000000000001407235022200234365ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/csrverifier/executable/csrverifier.go000066400000000000000000000030031407235022200263040ustar00rootroot00000000000000// Package executablecsrverifier defines the ExecutableCSRVerifier csrverifier.CSRVerifier. package executablecsrverifier import ( "errors" "os" "os/exec" "github.com/go-kit/kit/log" ) const ( userExecute os.FileMode = 1 << (6 - 3*iota) groupExecute otherExecute ) // New creates a executablecsrverifier.ExecutableCSRVerifier. func New(path string, logger log.Logger) (*ExecutableCSRVerifier, error) { fileInfo, err := os.Stat(path) if err != nil { return nil, err } fileMode := fileInfo.Mode() if fileMode.IsDir() { return nil, errors.New("CSR Verifier executable is a directory") } filePerm := fileMode.Perm() if filePerm&(userExecute|groupExecute|otherExecute) == 0 { return nil, errors.New("CSR Verifier executable is not executable") } return &ExecutableCSRVerifier{executable: path, logger: logger}, nil } // ExecutableCSRVerifier implements a csrverifier.CSRVerifier. // It executes a command, and passes it the raw decrypted CSR. // If the command exit code is 0, the CSR is considered valid. // In any other cases, the CSR is considered invalid. type ExecutableCSRVerifier struct { executable string logger log.Logger } func (v *ExecutableCSRVerifier) Verify(data []byte) (bool, error) { cmd := exec.Command(v.executable) stdin, err := cmd.StdinPipe() if err != nil { return false, err } go func() { defer stdin.Close() stdin.Write(data) }() err = cmd.Run() if err != nil { v.logger.Log("err", err) // mask the executable error return false, nil } return true, err } golang-github-micromdm-scep-2.0.0/depot/000077500000000000000000000000001407235022200201055ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/depot/bolt/000077500000000000000000000000001407235022200210455ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/depot/bolt/depot.go000066400000000000000000000170711407235022200225150ustar00rootroot00000000000000package bolt import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "errors" "fmt" "math/big" "time" "github.com/micromdm/scep/v2/cryptoutil" "github.com/boltdb/bolt" ) // Depot implements a SCEP certifiacte store using boltdb. // https://github.com/boltdb/bolt type Depot struct { *bolt.DB } const ( certBucket = "scep_certificates" ) // NewBoltDepot creates a depot.Depot backed by BoltDB. func NewBoltDepot(db *bolt.DB) (*Depot, error) { err := db.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(certBucket)) if err != nil { return fmt.Errorf("create bucket: %s", err) } return nil }) if err != nil { return nil, err } return &Depot{db}, nil } // For some read operations Bolt returns a direct memory reference to // the underlying mmap. This means that persistent references to these // memory locations are volatile. Make sure to copy data for places we // know references to this memeory will be kept. func bucketGetCopy(b *bolt.Bucket, key []byte) (out []byte) { in := b.Get(key) if in == nil { return } out = make([]byte, len(in)) copy(out, in) return } func (db *Depot) CA(pass []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) { chain := []*x509.Certificate{} var key *rsa.PrivateKey err := db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } // get ca_certificate caCert := bucketGetCopy(bucket, []byte("ca_certificate")) if caCert == nil { return fmt.Errorf("no ca_certificate in bucket") } cert, err := x509.ParseCertificate(caCert) if err != nil { return err } chain = append(chain, cert) // get ca_key caKey := bucket.Get([]byte("ca_key")) if caKey == nil { return fmt.Errorf("no ca_key in bucket") } key, err = x509.ParsePKCS1PrivateKey(caKey) if err != nil { return err } return nil }) if err != nil { return nil, nil, err } return chain, key, nil } func (db *Depot) Put(cn string, crt *x509.Certificate) error { if crt == nil || crt.Raw == nil { return fmt.Errorf("%q does not specify a valid certificate for storage", cn) } serial, err := db.Serial() if err != nil { return err } err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } name := cn + "." + serial.String() return bucket.Put([]byte(name), crt.Raw) }) if err != nil { return err } return db.incrementSerial(serial) } func (db *Depot) Serial() (*big.Int, error) { s := big.NewInt(2) if !db.hasKey([]byte("serial")) { if err := db.writeSerial(s); err != nil { return nil, err } return s, nil } err := db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } k := bucket.Get([]byte("serial")) if k == nil { return fmt.Errorf("key %q not found", "serial") } s = s.SetBytes(k) return nil }) if err != nil { return nil, err } return s, nil } func (db *Depot) writeSerial(s *big.Int) error { err := db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } return bucket.Put([]byte("serial"), []byte(s.Bytes())) }) return err } func (db *Depot) hasKey(name []byte) bool { var present bool db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } k := bucket.Get([]byte("serial")) if k != nil { present = true } return nil }) return present } func (db *Depot) incrementSerial(s *big.Int) error { serial := s.Add(s, big.NewInt(1)) err := db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } return bucket.Put([]byte("serial"), []byte(serial.Bytes())) }) return err } func (db *Depot) HasCN(cn string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) { // TODO: implement allowTime // TODO: implement revocation if cert == nil { return false, errors.New("nil certificate provided") } var hasCN bool err := db.View(func(tx *bolt.Tx) error { // TODO: "scep_certificates" is internal const in micromdm/scep curs := tx.Bucket([]byte("scep_certificates")).Cursor() prefix := []byte(cert.Subject.CommonName) for k, v := curs.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = curs.Next() { if bytes.Compare(v, cert.Raw) == 0 { hasCN = true return nil } } return nil }) return hasCN, err } func (db *Depot) CreateOrLoadKey(bits int) (*rsa.PrivateKey, error) { var ( key *rsa.PrivateKey err error ) err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } priv := bucket.Get([]byte("ca_key")) if priv == nil { return nil } key, err = x509.ParsePKCS1PrivateKey(priv) return err }) if err != nil { return nil, err } if key != nil { return key, nil } key, err = rsa.GenerateKey(rand.Reader, bits) if err != nil { return nil, err } err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } return bucket.Put([]byte("ca_key"), x509.MarshalPKCS1PrivateKey(key)) }) if err != nil { return nil, err } return key, nil } func (db *Depot) CreateOrLoadCA(key *rsa.PrivateKey, years int, org, country string) (*x509.Certificate, error) { var ( cert *x509.Certificate err error ) err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } caCert := bucketGetCopy(bucket, []byte("ca_certificate")) if caCert == nil { return nil } cert, err = x509.ParseCertificate(caCert) return err }) if err != nil { return nil, err } if cert != nil { return cert, nil } subject := pkix.Name{ Country: []string{country}, Organization: []string{org}, OrganizationalUnit: []string{"MICROMDM SCEP CA"}, Locality: nil, Province: nil, StreetAddress: nil, PostalCode: nil, SerialNumber: "", CommonName: org, } subjectKeyID, err := cryptoutil.GenerateSubjectKeyID(&key.PublicKey) if err != nil { return nil, err } authTemplate := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: subject, NotBefore: time.Now().Add(-600).UTC(), NotAfter: time.Now().AddDate(years, 0, 0).UTC(), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment, ExtKeyUsage: nil, UnknownExtKeyUsage: nil, BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, SubjectKeyId: subjectKeyID, DNSNames: nil, PermittedDNSDomainsCritical: false, PermittedDNSDomains: nil, } crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, &key.PublicKey, key) if err != nil { return nil, err } err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } return bucket.Put([]byte("ca_certificate"), crtBytes) }) if err != nil { return nil, err } return x509.ParseCertificate(crtBytes) } golang-github-micromdm-scep-2.0.0/depot/bolt/depot_test.go000066400000000000000000000056311407235022200235530ustar00rootroot00000000000000package bolt import ( "io/ioutil" "math/big" "os" "reflect" "testing" "github.com/boltdb/bolt" ) // createDepot creates a Bolt database in a temporary location. func createDB(mode os.FileMode, options *bolt.Options) *Depot { // Create temporary path. f, _ := ioutil.TempFile("", "bolt-") f.Close() os.Remove(f.Name()) db, err := bolt.Open(f.Name(), mode, options) if err != nil { panic(err.Error()) } d, err := NewBoltDepot(db) if err != nil { panic(err.Error()) } return d } func TestDepot_Serial(t *testing.T) { db := createDB(0666, nil) tests := []struct { name string want *big.Int wantErr bool }{ { name: "two is the default value.", want: big.NewInt(2), }, } for _, tt := range tests { got, err := db.Serial() if (err != nil) != tt.wantErr { t.Errorf("%q. Depot.Serial() error = %v, wantErr %v", tt.name, err, tt.wantErr) continue } if !reflect.DeepEqual(got, tt.want) { t.Errorf("%q. Depot.Serial() = %v, want %v", tt.name, got, tt.want) } } } func TestDepot_writeSerial(t *testing.T) { db := createDB(0666, nil) type args struct { s *big.Int } tests := []struct { name string args *big.Int wantErr bool }{ { args: big.NewInt(5), }, { args: big.NewInt(3), }, } for _, tt := range tests { if err := db.writeSerial(tt.args); (err != nil) != tt.wantErr { t.Errorf("%q. Depot.writeSerial() error = %v, wantErr %v", tt.name, err, tt.wantErr) } } } func TestDepot_incrementSerial(t *testing.T) { db := createDB(0666, nil) type args struct { s *big.Int } tests := []struct { name string args *big.Int want *big.Int wantErr bool }{ { args: big.NewInt(2), want: big.NewInt(3), }, { args: big.NewInt(3), want: big.NewInt(4), }, } for _, tt := range tests { if err := db.incrementSerial(tt.args); (err != nil) != tt.wantErr { t.Errorf("%q. Depot.incrementSerial() error = %v, wantErr %v", tt.name, err, tt.wantErr) } got, _ := db.Serial() if !reflect.DeepEqual(got, tt.want) { t.Errorf("%q. Depot.Serial() = %v, want %v", tt.name, got, tt.want) } } } func TestDepot_CreateOrLoadKey(t *testing.T) { db := createDB(0666, nil) tests := []struct { bits int wantErr bool }{ { bits: 1024, }, { bits: 2048, }, } for i, tt := range tests { if _, err := db.CreateOrLoadKey(tt.bits); (err != nil) != tt.wantErr { t.Errorf("%d. Depot.CreateOrLoadKey() error = %v, wantErr %v", i, err, tt.wantErr) } } } func TestDepot_CreateOrLoadCA(t *testing.T) { db := createDB(0666, nil) tests := []struct { wantErr bool }{ {}, {}, } for i, tt := range tests { key, err := db.CreateOrLoadKey(1024) if err != nil { t.Fatalf("%d. Depot.CreateOrLoadKey() error = %v", i, err) } if _, err := db.CreateOrLoadCA(key, 10, "MicroMDM", "US"); (err != nil) != tt.wantErr { t.Errorf("%d. Depot.CreateOrLoadCA() error = %v, wantErr %v", i, err, tt.wantErr) } } } golang-github-micromdm-scep-2.0.0/depot/depot.go000066400000000000000000000005741407235022200215550ustar00rootroot00000000000000package depot import ( "crypto/rsa" "crypto/x509" "math/big" ) // Depot is a repository for managing certificates type Depot interface { CA(pass []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) Put(name string, crt *x509.Certificate) error Serial() (*big.Int, error) HasCN(cn string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) } golang-github-micromdm-scep-2.0.0/depot/file/000077500000000000000000000000001407235022200210245ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/depot/file/depot.go000066400000000000000000000240301407235022200224650ustar00rootroot00000000000000package file import ( "bufio" "bytes" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/pem" "errors" "fmt" "io" "io/ioutil" "math/big" "os" "path/filepath" "strconv" "strings" "time" ) // NewFileDepot returns a new cert depot. func NewFileDepot(path string) (*fileDepot, error) { f, err := os.OpenFile(fmt.Sprintf("%s/index.txt", path), os.O_RDONLY|os.O_CREATE, 0666) if err != nil { return nil, err } defer f.Close() return &fileDepot{dirPath: path}, nil } type fileDepot struct { dirPath string } func (d *fileDepot) CA(pass []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) { caPEM, err := d.getFile("ca.pem") if err != nil { return nil, nil, err } cert, err := loadCert(caPEM.Data) if err != nil { return nil, nil, err } keyPEM, err := d.getFile("ca.key") if err != nil { return nil, nil, err } key, err := loadKey(keyPEM.Data, pass) if err != nil { return nil, nil, err } return []*x509.Certificate{cert}, key, nil } // file permissions const ( certPerm = 0444 serialPerm = 0400 dbPerm = 0600 ) // Put adds a certificate to the depot func (d *fileDepot) Put(cn string, crt *x509.Certificate) error { if crt == nil { return errors.New("crt is nil") } if crt.Raw == nil { return errors.New("data is nil") } data := crt.Raw if err := os.MkdirAll(d.dirPath, 0755); err != nil { return err } serial, err := d.Serial() if err != nil { return err } if crt.Subject.CommonName == "" { // this means our cn was replaced by the certificate Signature // which is inappropriate for a filename cn = fmt.Sprintf("%x", sha256.Sum256(crt.Raw)) } filename := fmt.Sprintf("%s.%s.pem", cn, serial.String()) filepath := d.path(filename) file, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, certPerm) if err != nil { return err } defer file.Close() if _, err := file.Write(pemCert(data)); err != nil { os.Remove(filepath) return err } if err := d.writeDB(cn, serial, filename, crt); err != nil { // TODO : remove certificate in case of writeDB problems return err } if err := d.incrementSerial(serial); err != nil { return err } return nil } func (d *fileDepot) Serial() (*big.Int, error) { name := d.path("serial") s := big.NewInt(2) if err := d.check("serial"); err != nil { // assuming it doesnt exist, create if err := d.writeSerial(s); err != nil { return nil, err } return s, nil } file, err := os.Open(name) if err != nil { return nil, err } defer file.Close() r := bufio.NewReader(file) data, err := r.ReadString('\r') if err != nil && err != io.EOF { return nil, err } data = strings.TrimSuffix(data, "\r") data = strings.TrimSuffix(data, "\n") serial, ok := s.SetString(data, 16) if !ok { return nil, errors.New("could not convert " + string(data) + " to serial number") } return serial, nil } func makeOpenSSLTime(t time.Time) string { y := (int(t.Year()) % 100) validDate := fmt.Sprintf("%02d%02d%02d%02d%02d%02dZ", y, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) return validDate } func makeDn(cert *x509.Certificate) string { var dn bytes.Buffer if len(cert.Subject.Country) > 0 && len(cert.Subject.Country[0]) > 0 { dn.WriteString("/C=" + cert.Subject.Country[0]) } if len(cert.Subject.Province) > 0 && len(cert.Subject.Province[0]) > 0 { dn.WriteString("/ST=" + cert.Subject.Province[0]) } if len(cert.Subject.Locality) > 0 && len(cert.Subject.Locality[0]) > 0 { dn.WriteString("/L=" + cert.Subject.Locality[0]) } if len(cert.Subject.Organization) > 0 && len(cert.Subject.Organization[0]) > 0 { dn.WriteString("/O=" + cert.Subject.Organization[0]) } if len(cert.Subject.OrganizationalUnit) > 0 && len(cert.Subject.OrganizationalUnit[0]) > 0 { dn.WriteString("/OU=" + cert.Subject.OrganizationalUnit[0]) } if len(cert.Subject.CommonName) > 0 { dn.WriteString("/CN=" + cert.Subject.CommonName) } if len(cert.EmailAddresses) > 0 { dn.WriteString("/emailAddress=" + cert.EmailAddresses[0]) } return dn.String() } // Determine if the cadb already has a valid certificate with the same name func (d *fileDepot) HasCN(_ string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) { var addDB bytes.Buffer candidates := make(map[string]string) dn := makeDn(cert) if err := os.MkdirAll(d.dirPath, 0755); err != nil { return false, err } name := d.path("index.txt") file, err := os.Open(name) if err != nil { return false, err } defer file.Close() // Loop over index.txt, determine if a certificate is valid and can be revoked // revoke certificate in DB if requested scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasSuffix(line, dn) { // Removing revoked certificate from candidates, if any if strings.HasPrefix(line, "R\t") { entries := strings.Split(line, "\t") serial := strings.ToUpper(entries[3]) candidates[serial] = line delete(candidates, serial) addDB.WriteString(line + "\n") // Test & add certificate candidates, if any } else if strings.HasPrefix(line, "V\t") { issueDate, err := strconv.ParseInt(strings.Replace(strings.Split(line, "\t")[1], "Z", "", 1), 10, 64) if err != nil { return false, errors.New("Could not get expiry date from ca db") } minimalRenewDate, err := strconv.ParseInt(strings.Replace(makeOpenSSLTime(time.Now().AddDate(0, 0, allowTime).UTC()), "Z", "", 1), 10, 64) if err != nil { return false, errors.New("Could not calculate expiry date") } entries := strings.Split(line, "\t") serial := strings.ToUpper(entries[3]) // all non renewable certificates if minimalRenewDate < issueDate && allowTime > 0 { candidates[serial] = "no" } else { candidates[serial] = line } } } else { addDB.WriteString(line + "\n") } } file.Close() for key, value := range candidates { if value == "no" { return false, errors.New("DN " + dn + " already exists") } if revokeOldCertificate { fmt.Println("Revoking certificate with serial " + key + " from DB. Recreation of CRL needed.") entries := strings.Split(value, "\t") addDB.WriteString("R\t" + entries[1] + "\t" + makeOpenSSLTime(time.Now()) + "\t" + strings.ToUpper(entries[3]) + "\t" + entries[4] + "\t" + entries[5] + "\n") } } if err := scanner.Err(); err != nil { return false, err } if revokeOldCertificate { file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, dbPerm) if err != nil { return false, err } if _, err := file.Write(addDB.Bytes()); err != nil { return false, err } } return true, nil } func (d *fileDepot) writeDB(cn string, serial *big.Int, filename string, cert *x509.Certificate) error { var dbEntry bytes.Buffer // Revoke old certificate if _, err := d.HasCN(cn, 0, cert, true); err != nil { return err } if err := os.MkdirAll(d.dirPath, 0755); err != nil { return err } name := d.path("index.txt") file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_APPEND, dbPerm) if err != nil { return fmt.Errorf("could not append to "+name+" : %q\n", err.Error()) } defer file.Close() // Format of the caDB, see http://pki-tutorial.readthedocs.io/en/latest/cadb.html // STATUSFLAG EXPIRATIONDATE REVOCATIONDATE(or emtpy) SERIAL_IN_HEX CERTFILENAME_OR_'unknown' Certificate_DN serialHex := fmt.Sprintf("%X", cert.SerialNumber) if len(serialHex)%2 == 1 { serialHex = fmt.Sprintf("0%s", serialHex) } validDate := makeOpenSSLTime(cert.NotAfter) dn := makeDn(cert) // Valid dbEntry.WriteString("V\t") // Valid till dbEntry.WriteString(validDate + "\t") // Empty (not revoked) dbEntry.WriteString("\t") // Serial in Hex dbEntry.WriteString(serialHex + "\t") // Certificate file name dbEntry.WriteString(filename + "\t") // Certificate DN dbEntry.WriteString(dn) dbEntry.WriteString("\n") if _, err := file.Write(dbEntry.Bytes()); err != nil { return err } return nil } func (d *fileDepot) writeSerial(serial *big.Int) error { if err := os.MkdirAll(d.dirPath, 0755); err != nil { return err } name := d.path("serial") os.Remove(name) file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, serialPerm) if err != nil { return err } defer file.Close() if _, err := file.WriteString(fmt.Sprintf("%x\n", serial.Bytes())); err != nil { os.Remove(name) return err } return nil } // read serial and increment func (d *fileDepot) incrementSerial(s *big.Int) error { serial := s.Add(s, big.NewInt(1)) if err := d.writeSerial(serial); err != nil { return err } return nil } type file struct { Info os.FileInfo Data []byte } func (d *fileDepot) check(path string) error { name := d.path(path) _, err := os.Stat(name) if err != nil { return err } return nil } func (d *fileDepot) getFile(path string) (*file, error) { if err := d.check(path); err != nil { return nil, err } fi, err := os.Stat(d.path(path)) if err != nil { return nil, err } b, err := ioutil.ReadFile(d.path(path)) return &file{fi, b}, err } func (d *fileDepot) path(name string) string { return filepath.Join(d.dirPath, name) } const ( rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" certificatePEMBlockType = "CERTIFICATE" ) // load an encrypted private key from disk func loadKey(data []byte, password []byte) (*rsa.PrivateKey, error) { pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode failed") } if pemBlock.Type != rsaPrivateKeyPEMBlockType { return nil, errors.New("unmatched type or headers") } b, err := x509.DecryptPEMBlock(pemBlock, password) if err != nil { return nil, err } return x509.ParsePKCS1PrivateKey(b) } // load an encrypted private key from disk func loadCert(data []byte) (*x509.Certificate, error) { pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode failed") } if pemBlock.Type != certificatePEMBlockType { return nil, errors.New("unmatched type or headers") } return x509.ParseCertificate(pemBlock.Bytes) } func pemCert(derBytes []byte) []byte { pemBlock := &pem.Block{ Type: certificatePEMBlockType, Headers: nil, Bytes: derBytes, } out := pem.EncodeToMemory(pemBlock) return out } golang-github-micromdm-scep-2.0.0/depot/signer.go000066400000000000000000000053721407235022200217320ustar00rootroot00000000000000package depot import ( "crypto/rand" "crypto/x509" "time" "github.com/micromdm/scep/v2/cryptoutil" "github.com/micromdm/scep/v2/scep" ) // Signer signs x509 certificates and stores them in a Depot type Signer struct { depot Depot caPass string allowRenewalDays int validityDays int } // Option customizes Signer type Option func(*Signer) // NewSigner creates a new Signer func NewSigner(depot Depot, opts ...Option) *Signer { s := &Signer{ depot: depot, allowRenewalDays: 14, validityDays: 365, } for _, opt := range opts { opt(s) } return s } // WithCAPass specifies the password to use with an encrypted CA key func WithCAPass(pass string) Option { return func(s *Signer) { s.caPass = pass } } // WithAllowRenewalDays sets the allowable renewal time for existing certs func WithAllowRenewalDays(r int) Option { return func(s *Signer) { s.allowRenewalDays = r } } // WithValidityDays sets the validity period new certs will use func WithValidityDays(v int) Option { return func(s *Signer) { s.validityDays = v } } // SignCSR signs a certificate using Signer's Depot CA func (s *Signer) SignCSR(m *scep.CSRReqMessage) (*x509.Certificate, error) { id, err := cryptoutil.GenerateSubjectKeyID(m.CSR.PublicKey) if err != nil { return nil, err } serial, err := s.depot.Serial() if err != nil { return nil, err } // create cert template tmpl := &x509.Certificate{ SerialNumber: serial, Subject: m.CSR.Subject, NotBefore: time.Now().Add(-600).UTC(), NotAfter: time.Now().AddDate(0, 0, s.validityDays).UTC(), SubjectKeyId: id, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, }, SignatureAlgorithm: m.CSR.SignatureAlgorithm, DNSNames: m.CSR.DNSNames, EmailAddresses: m.CSR.EmailAddresses, IPAddresses: m.CSR.IPAddresses, URIs: m.CSR.URIs, } caCerts, caKey, err := s.depot.CA([]byte(s.caPass)) if err != nil { return nil, err } crtBytes, err := x509.CreateCertificate(rand.Reader, tmpl, caCerts[0], m.CSR.PublicKey, caKey) if err != nil { return nil, err } 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 allowRenewalDays _, err = s.depot.HasCN(name, s.allowRenewalDays, crt, false) if err != nil { return nil, err } if err := s.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) } golang-github-micromdm-scep-2.0.0/go.mod000066400000000000000000000014321407235022200201000ustar00rootroot00000000000000module github.com/micromdm/scep/v2 go 1.16 require ( github.com/boltdb/bolt v1.3.1 github.com/go-kit/kit v0.4.0 github.com/go-logfmt/logfmt v0.3.0 // indirect github.com/go-stack/stack v1.6.0 // indirect github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect github.com/gorilla/mux v1.4.0 github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect github.com/pkg/errors v0.8.0 go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 // indirect golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5 // indirect ) replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568 golang-github-micromdm-scep-2.0.0/go.sum000066400000000000000000000043661407235022200201360ustar00rootroot00000000000000github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/go-kit/kit v0.4.0 h1:KeVK+Emj3c3S4eRztFuzbFYb2BAgf2jmwDwyXEri7Lo= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-stack/stack v1.6.0 h1:MmJCxYVKTJ0SplGKqFVX3SBnmaUhODHZrrFF6jMbpZk= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.4.0 h1:N6R8isjoRv7IcVVlf0cTBbo0UDc9V6ZXWEm0HQoQmLo= github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568 h1:+MPqEswjYiS0S1FCTg8MIhMBMzxiVQ94rooFwvPPiWk= github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 h1:1Pw+ZX4dmGORIwGkTwnUr7RFuMhfpCYHXRZNF04XPYs= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5 h1:NAjcSWsnFBcOQGn/lxvHouhL7iPC53X8+znVzzQkAEg= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang-github-micromdm-scep-2.0.0/scep/000077500000000000000000000000001407235022200177245ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/scep/certs_selector.go000066400000000000000000000030701407235022200232730ustar00rootroot00000000000000package scep import ( "crypto/sha256" "crypto/x509" ) // A CertsSelector filters certificates. type CertsSelector interface { SelectCerts([]*x509.Certificate) []*x509.Certificate } // CertsSelectorFunc is a type of function that filters certificates. type CertsSelectorFunc func([]*x509.Certificate) []*x509.Certificate func (f CertsSelectorFunc) SelectCerts(certs []*x509.Certificate) []*x509.Certificate { return f(certs) } // NopCertsSelector returns a CertsSelectorFunc that does not do anything. func NopCertsSelector() CertsSelectorFunc { return func(certs []*x509.Certificate) []*x509.Certificate { return certs } } // A EnciphermentCertsSelector returns a CertsSelectorFunc that selects // certificates eligible for key encipherment. This certsSelector can be used // to filter PKCSReq recipients. func EnciphermentCertsSelector() CertsSelectorFunc { return func(certs []*x509.Certificate) (selected []*x509.Certificate) { enciphermentKeyUsages := x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment for _, cert := range certs { if cert.KeyUsage&enciphermentKeyUsages != 0 { selected = append(selected, cert) } } return selected } } // SHA256FingerprintCertsSelector selects a certificate that matches // a SHA-256 hash of the raw certificate DER bytes func SHA256FingerprintCertsSelector(hash [32]byte) CertsSelectorFunc { return func(certs []*x509.Certificate) (selected []*x509.Certificate) { for _, cert := range certs { if sha256.Sum256(cert.Raw) == hash { selected = append(selected, cert) return } } return } } golang-github-micromdm-scep-2.0.0/scep/certs_selector_test.go000066400000000000000000000050471407235022200243400ustar00rootroot00000000000000package scep import ( "crypto/x509" "testing" ) func TestEnciphermentCertsSelector(t *testing.T) { for _, test := range []struct { testName string certs []*x509.Certificate expectedSelectedCerts []*x509.Certificate }{ { "empty certificates list", []*x509.Certificate{}, []*x509.Certificate{}, }, { "non-empty certificates list", []*x509.Certificate{ {KeyUsage: x509.KeyUsageKeyEncipherment}, {KeyUsage: x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageDigitalSignature}, {}, }, []*x509.Certificate{ {KeyUsage: x509.KeyUsageKeyEncipherment}, {KeyUsage: x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, }, }, } { test := test t.Run(test.testName, func(t *testing.T) { t.Parallel() selected := EnciphermentCertsSelector().SelectCerts(test.certs) if !certsKeyUsagesEq(selected, test.expectedSelectedCerts) { t.Fatal("selected and expected certificates did not match") } }) } } func TestNopCertsSelector(t *testing.T) { for _, test := range []struct { testName string certs []*x509.Certificate expectedSelectedCerts []*x509.Certificate }{ { "empty certificates list", []*x509.Certificate{}, []*x509.Certificate{}, }, { "non-empty certificates list", []*x509.Certificate{ {KeyUsage: x509.KeyUsageKeyEncipherment}, {KeyUsage: x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageDigitalSignature}, {}, }, []*x509.Certificate{ {KeyUsage: x509.KeyUsageKeyEncipherment}, {KeyUsage: x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, {KeyUsage: x509.KeyUsageDigitalSignature}, {}, }, }, } { test := test t.Run(test.testName, func(t *testing.T) { t.Parallel() selected := NopCertsSelector().SelectCerts(test.certs) if !certsKeyUsagesEq(selected, test.expectedSelectedCerts) { t.Fatal("selected and expected certificates did not match") } }) } } // certsKeyUsagesEq returns true if certs in a have the same key usages // of certs in b and in the same order. func certsKeyUsagesEq(a []*x509.Certificate, b []*x509.Certificate) bool { if len(a) != len(b) { return false } for i, cert := range a { if cert.KeyUsage != b[i].KeyUsage { return false } } return true } golang-github-micromdm-scep-2.0.0/scep/scep.go000066400000000000000000000400301407235022200212020ustar00rootroot00000000000000// Package scep provides common functionality for encoding and decoding // Simple Certificate Enrolment Protocol pki messages as defined by // https://tools.ietf.org/html/draft-gutmann-scep-02 package scep import ( "bytes" "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/asn1" "encoding/base64" "github.com/micromdm/scep/v2/cryptoutil" "github.com/micromdm/scep/v2/cryptoutil/x509util" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" "go.mozilla.org/pkcs7" ) // errors var ( errNotImplemented = errors.New("not implemented") errUnknownMessageType = errors.New("unknown messageType") ) // The MessageType attribute specifies the type of operation performed // by the transaction. This attribute MUST be included in all PKI // messages. // // The following message types are defined: type MessageType string // Undefined message types are treated as an error. const ( CertRep MessageType = "3" RenewalReq = "17" UpdateReq = "18" PKCSReq = "19" CertPoll = "20" GetCert = "21" GetCRL = "22" ) func (msg MessageType) String() string { switch msg { case CertRep: return "CertRep (3)" case RenewalReq: return "RenewalReq (17)" case UpdateReq: return "UpdateReq (18)" case PKCSReq: return "PKCSReq (19)" case CertPoll: return "CertPoll (20) " case GetCert: return "GetCert (21)" case GetCRL: return "GetCRL (22)" default: panic("scep: unknown messageType" + msg) } } // PKIStatus is a SCEP pkiStatus attribute which holds transaction status information. // All SCEP responses MUST include a pkiStatus. // // The following pkiStatuses are defined: type PKIStatus string // Undefined pkiStatus attributes are treated as an error const ( SUCCESS PKIStatus = "0" FAILURE = "2" PENDING = "3" ) // FailInfo is a SCEP failInfo attribute // // The FailInfo attribute MUST contain one of the following failure // reasons: type FailInfo string // const ( BadAlg FailInfo = "0" BadMessageCheck = "1" BadRequest = "2" BadTime = "3" BadCertID = "4" ) func (info FailInfo) String() string { switch info { case BadAlg: return "badAlg (0)" case BadMessageCheck: return "badMessageCheck (1)" case BadRequest: return "badRequest (2)" case BadTime: return "badTime (3)" case BadCertID: return "badCertID (4)" default: panic("scep: unknown failInfo type" + info) } } // SenderNonce is a random 16 byte number. // A sender must include the senderNonce in each transaction to a recipient. type SenderNonce []byte // The RecipientNonce MUST be copied from the SenderNonce // and included in the reply. type RecipientNonce []byte // The TransactionID is a text // string generated by the client when starting a transaction. The // client MUST generate a unique string as the transaction identifier, // which MUST be used for all PKI messages exchanged for a given // enrolment, encoded as a PrintableString. type TransactionID string // SCEP OIDs var ( oidSCEPmessageType = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 2} oidSCEPpkiStatus = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 3} oidSCEPfailInfo = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 4} oidSCEPsenderNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 5} oidSCEPrecipientNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 6} oidSCEPtransactionID = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 7} ) // WithLogger adds option logging to the SCEP operations. func WithLogger(logger log.Logger) Option { return func(c *config) { c.logger = logger } } // WithCACerts adds option CA certificates to the SCEP operations. // Note: This changes the verification behavior of PKCS #7 messages. If this // option is specified, only caCerts will be used as expected signers. func WithCACerts(caCerts []*x509.Certificate) Option { return func(c *config) { c.caCerts = caCerts } } // WithCertsSelector adds the certificates certsSelector option to the SCEP // operations. // This option is effective when used with NewCSRRequest function. In // this case, only certificates selected with the certsSelector will be used // as the PKCS #7 message recipients. func WithCertsSelector(selector CertsSelector) Option { return func(c *config) { c.certsSelector = selector } } // Option specifies custom configuration for SCEP. type Option func(*config) type config struct { logger log.Logger caCerts []*x509.Certificate // specified if CA certificates have already been retrieved certsSelector CertsSelector } // PKIMessage defines the possible SCEP message types type PKIMessage struct { TransactionID MessageType SenderNonce *CertRepMessage *CSRReqMessage // DER Encoded PKIMessage Raw []byte // parsed p7 *pkcs7.PKCS7 // decrypted enveloped content pkiEnvelope []byte // Used to encrypt message Recipients []*x509.Certificate // Signer info SignerKey *rsa.PrivateKey SignerCert *x509.Certificate logger log.Logger } // CertRepMessage is a type of PKIMessage type CertRepMessage struct { PKIStatus RecipientNonce FailInfo Certificate *x509.Certificate degenerate []byte } // CSRReqMessage can be of the type PKCSReq/RenewalReq/UpdateReq // and includes a PKCS#10 CSR request. // The content of this message is protected // by the recipient public key(example CA) type CSRReqMessage struct { RawDecrypted []byte // PKCS#10 Certificate request inside the envelope CSR *x509.CertificateRequest ChallengePassword string } // ParsePKIMessage unmarshals a PKCS#7 signed data into a PKI message struct func ParsePKIMessage(data []byte, opts ...Option) (*PKIMessage, error) { conf := &config{logger: log.NewNopLogger()} for _, opt := range opts { opt(conf) } // parse PKCS#7 signed data p7, err := pkcs7.Parse(data) if err != nil { return nil, err } if len(conf.caCerts) > 0 { // According to RFC #2315 Section 9.1, it is valid that the server sends fewer // certificates than necessary, if it is expected that those verifying the // signatures have an alternate means of obtaining necessary certificates. // In SCEP case, an alternate means is to use GetCaCert request. // Note: The https://github.com/jscep/jscep implementation logs a warning if // no certificates were found for signers in the PKCS #7 received from the // server, but the certificates obtained from GetCaCert request are still // used for decoding the message. p7.Certificates = conf.caCerts } if err := p7.Verify(); err != nil { return nil, err } var tID TransactionID if err := p7.UnmarshalSignedAttribute(oidSCEPtransactionID, &tID); err != nil { return nil, err } var msgType MessageType if err := p7.UnmarshalSignedAttribute(oidSCEPmessageType, &msgType); err != nil { return nil, err } msg := &PKIMessage{ TransactionID: tID, MessageType: msgType, Raw: data, p7: p7, logger: conf.logger, } // log relevant key-values when parsing a pkiMessage. logKeyVals := []interface{}{ "msg", "parsed scep pkiMessage", "scep_message_type", msgType, "transaction_id", tID, } level.Debug(msg.logger).Log(logKeyVals...) if err := msg.parseMessageType(); err != nil { return nil, err } return msg, nil } func (msg *PKIMessage) parseMessageType() error { switch msg.MessageType { case CertRep: var status PKIStatus if err := msg.p7.UnmarshalSignedAttribute(oidSCEPpkiStatus, &status); err != nil { return err } var rn RecipientNonce if err := msg.p7.UnmarshalSignedAttribute(oidSCEPrecipientNonce, &rn); err != nil { return err } if len(rn) == 0 { return errors.New("scep pkiMessage must include recipientNonce attribute") } cr := &CertRepMessage{ PKIStatus: status, RecipientNonce: rn, } switch status { case SUCCESS: break case FAILURE: var fi FailInfo if err := msg.p7.UnmarshalSignedAttribute(oidSCEPfailInfo, &fi); err != nil { return err } if fi == "" { return errors.New("scep pkiStatus FAILURE must have a failInfo attribute") } cr.FailInfo = fi case PENDING: break default: return errors.Errorf("unknown scep pkiStatus %s", status) } msg.CertRepMessage = cr return nil case PKCSReq, UpdateReq, RenewalReq: var sn SenderNonce if err := msg.p7.UnmarshalSignedAttribute(oidSCEPsenderNonce, &sn); err != nil { return err } if len(sn) == 0 { return errors.New("scep pkiMessage must include senderNonce attribute") } msg.SenderNonce = sn return nil case GetCRL, GetCert, CertPoll: return errNotImplemented default: return errUnknownMessageType } } // DecryptPKIEnvelope decrypts the pkcs envelopedData inside the SCEP PKIMessage func (msg *PKIMessage) DecryptPKIEnvelope(cert *x509.Certificate, key *rsa.PrivateKey) error { p7, err := pkcs7.Parse(msg.p7.Content) if err != nil { return err } msg.pkiEnvelope, err = p7.Decrypt(cert, key) if err != nil { return err } logKeyVals := []interface{}{ "msg", "decrypt pkiEnvelope", } defer func() { level.Debug(msg.logger).Log(logKeyVals...) }() switch msg.MessageType { case CertRep: certs, err := CACerts(msg.pkiEnvelope) if err != nil { return err } msg.CertRepMessage.Certificate = certs[0] logKeyVals = append(logKeyVals, "ca_certs", len(certs)) return nil case PKCSReq, UpdateReq, RenewalReq: csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope) if err != nil { return errors.Wrap(err, "parse CSR from pkiEnvelope") } // check for challengePassword cp, err := x509util.ParseChallengePassword(msg.pkiEnvelope) if err != nil { return errors.Wrap(err, "scep: parse challenge password in pkiEnvelope") } msg.CSRReqMessage = &CSRReqMessage{ RawDecrypted: msg.pkiEnvelope, CSR: csr, ChallengePassword: cp, } logKeyVals = append(logKeyVals, "has_challenge", cp != "") return nil case GetCRL, GetCert, CertPoll: return errNotImplemented default: return errUnknownMessageType } } func (msg *PKIMessage) Fail(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, info FailInfo) (*PKIMessage, error) { config := pkcs7.SignerInfoConfig{ ExtraSignedAttributes: []pkcs7.Attribute{ { Type: oidSCEPtransactionID, Value: msg.TransactionID, }, { Type: oidSCEPpkiStatus, Value: FAILURE, }, { Type: oidSCEPfailInfo, Value: info, }, { Type: oidSCEPmessageType, Value: CertRep, }, { Type: oidSCEPsenderNonce, Value: msg.SenderNonce, }, { Type: oidSCEPrecipientNonce, Value: msg.SenderNonce, }, }, } sd, err := pkcs7.NewSignedData(nil) if err != nil { return nil, err } // sign the attributes if err := sd.AddSigner(crtAuth, keyAuth, config); err != nil { return nil, err } certRepBytes, err := sd.Finish() if err != nil { return nil, err } cr := &CertRepMessage{ PKIStatus: FAILURE, FailInfo: BadRequest, RecipientNonce: RecipientNonce(msg.SenderNonce), } // create a CertRep message from the original crepMsg := &PKIMessage{ Raw: certRepBytes, TransactionID: msg.TransactionID, MessageType: CertRep, CertRepMessage: cr, } return crepMsg, nil } // 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 } } // create a degenerate cert structure deg, err := DegenerateCertificates([]*x509.Certificate{crt}) if err != nil { return nil, err } // encrypt degenerate data using the original messages recipients e7, err := pkcs7.Encrypt(deg, msg.p7.Certificates) if err != nil { return nil, err } // PKIMessageAttributes to be signed config := pkcs7.SignerInfoConfig{ ExtraSignedAttributes: []pkcs7.Attribute{ { Type: oidSCEPtransactionID, Value: msg.TransactionID, }, { Type: oidSCEPpkiStatus, Value: SUCCESS, }, { Type: oidSCEPmessageType, Value: CertRep, }, { Type: oidSCEPsenderNonce, Value: msg.SenderNonce, }, { Type: oidSCEPrecipientNonce, Value: msg.SenderNonce, }, }, } signedData, err := pkcs7.NewSignedData(e7) if err != nil { return nil, err } // add the certificate into the signed data type // this cert must be added before the signedData because the recipient will expect it // as the first certificate in the array signedData.AddCertificate(crt) // sign the attributes if err := signedData.AddSigner(crtAuth, keyAuth, config); err != nil { return nil, err } certRepBytes, err := signedData.Finish() if err != nil { return nil, err } cr := &CertRepMessage{ PKIStatus: SUCCESS, RecipientNonce: RecipientNonce(msg.SenderNonce), Certificate: crt, degenerate: deg, } // create a CertRep message from the original crepMsg := &PKIMessage{ Raw: certRepBytes, TransactionID: msg.TransactionID, MessageType: CertRep, CertRepMessage: cr, } return crepMsg, nil } // DegenerateCertificates creates degenerate certificates pkcs#7 type func DegenerateCertificates(certs []*x509.Certificate) ([]byte, error) { var buf bytes.Buffer for _, cert := range certs { buf.Write(cert.Raw) } degenerate, err := pkcs7.DegenerateCertificate(buf.Bytes()) if err != nil { return nil, err } return degenerate, nil } // CACerts extract CA Certificate or chain from pkcs7 degenerate signed data func CACerts(data []byte) ([]*x509.Certificate, error) { p7, err := pkcs7.Parse(data) if err != nil { return nil, err } return p7.Certificates, nil } // NewCSRRequest creates a scep PKI PKCSReq/UpdateReq message func NewCSRRequest(csr *x509.CertificateRequest, tmpl *PKIMessage, opts ...Option) (*PKIMessage, error) { conf := &config{logger: log.NewNopLogger(), certsSelector: NopCertsSelector()} for _, opt := range opts { opt(conf) } derBytes := csr.Raw recipients := conf.certsSelector.SelectCerts(tmpl.Recipients) if len(recipients) < 1 { if len(tmpl.Recipients) >= 1 { // our certsSelector eliminated any CA/RA recipients return nil, errors.New("no selected CA/RA recipients") } return nil, errors.New("no CA/RA recipients") } e7, err := pkcs7.Encrypt(derBytes, recipients) if err != nil { return nil, err } signedData, err := pkcs7.NewSignedData(e7) if err != nil { return nil, err } // create transaction ID from public key hash tID, err := newTransactionID(csr.PublicKey) if err != nil { return nil, err } sn, err := newNonce() if err != nil { return nil, err } level.Debug(conf.logger).Log( "msg", "creating SCEP CSR request", "transaction_id", tID, "signer_cn", tmpl.SignerCert.Subject.CommonName, ) // PKIMessageAttributes to be signed config := pkcs7.SignerInfoConfig{ ExtraSignedAttributes: []pkcs7.Attribute{ { Type: oidSCEPtransactionID, Value: tID, }, { Type: oidSCEPmessageType, Value: tmpl.MessageType, }, { Type: oidSCEPsenderNonce, Value: sn, }, }, } // sign attributes if err := signedData.AddSigner(tmpl.SignerCert, tmpl.SignerKey, config); err != nil { return nil, err } rawPKIMessage, err := signedData.Finish() if err != nil { return nil, err } cr := &CSRReqMessage{ CSR: csr, } newMsg := &PKIMessage{ Raw: rawPKIMessage, MessageType: tmpl.MessageType, TransactionID: tID, SenderNonce: sn, CSRReqMessage: cr, Recipients: recipients, logger: conf.logger, } return newMsg, nil } func newNonce() (SenderNonce, error) { size := 16 b := make([]byte, size) _, err := rand.Read(b) if err != nil { return SenderNonce{}, err } return SenderNonce(b), nil } // use public key to create a deterministric transactionID func newTransactionID(key crypto.PublicKey) (TransactionID, error) { id, err := cryptoutil.GenerateSubjectKeyID(key) if err != nil { return "", err } encHash := base64.StdEncoding.EncodeToString(id) return TransactionID(encHash), nil } golang-github-micromdm-scep-2.0.0/scep/scep_test.go000066400000000000000000000224411407235022200222470ustar00rootroot00000000000000package scep_test import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "io/ioutil" "math/big" "testing" "time" "github.com/micromdm/scep/v2/cryptoutil" "github.com/micromdm/scep/v2/scep" ) func testParsePKIMessage(t *testing.T, data []byte) *scep.PKIMessage { msg, err := scep.ParsePKIMessage(data) if err != nil { t.Fatal(err) } validateParsedPKIMessage(t, msg) return msg } func validateParsedPKIMessage(t *testing.T, msg *scep.PKIMessage) { if msg.TransactionID == "" { t.Errorf("expected TransactionID attribute") } if msg.MessageType == "" { t.Errorf("expected MessageType attribute") } switch msg.MessageType { case scep.CertRep: if len(msg.RecipientNonce) == 0 { t.Errorf("expected RecipientNonce attribute") } case scep.PKCSReq, scep.UpdateReq, scep.RenewalReq: if len(msg.SenderNonce) == 0 { t.Errorf("expected SenderNonce attribute") } } } // Tests the case when servers reply with PKCS #7 signed-data that contains no // certificates assuming that the client can request CA certificates using // GetCaCert request. func TestParsePKIEnvelopeCert_MissingCertificatesForSigners(t *testing.T) { certRepMissingCertificates := loadTestFile(t, "testdata/testca2/CertRep_NoCertificatesForSigners.der") caPEM := loadTestFile(t, "testdata/testca2/ca2.pem") // Try to parse the PKIMessage without providing certificates for signers. _, err := scep.ParsePKIMessage(certRepMissingCertificates) if err == nil { t.Fatal("parsed PKIMessage without providing signer certificates") } signerCert := decodePEMCert(t, caPEM) msg, err := scep.ParsePKIMessage(certRepMissingCertificates, scep.WithCACerts([]*x509.Certificate{signerCert})) if err != nil { t.Fatalf("failed to parse PKIMessage: %v", err) } validateParsedPKIMessage(t, msg) } func TestDecryptPKIEnvelopeCSR(t *testing.T) { pkcsReq := loadTestFile(t, "testdata/PKCSReq.der") msg := testParsePKIMessage(t, pkcsReq) cacert, cakey := loadCACredentials(t) err := msg.DecryptPKIEnvelope(cacert, cakey) if err != nil { t.Fatal(err) } if msg.CSRReqMessage.CSR == nil { t.Errorf("expected non-nil CSR field") } } func TestDecryptPKIEnvelopeCert(t *testing.T) { certRep := loadTestFile(t, "testdata/CertRep.der") testParsePKIMessage(t, certRep) // clientcert, clientkey := loadClientCredentials(t) // err = msg.DecryptPKIEnvelope(clientcert, clientkey) // if err != nil { // t.Fatal(err) // } } func TestSignCSR(t *testing.T) { pkcsReq := loadTestFile(t, "testdata/PKCSReq.der") msg := testParsePKIMessage(t, pkcsReq) cacert, cakey := loadCACredentials(t) err := msg.DecryptPKIEnvelope(cacert, cakey) if err != nil { t.Fatal(err) } csr := msg.CSRReqMessage.CSR id, err := cryptoutil.GenerateSubjectKeyID(csr.PublicKey) if err != nil { t.Fatal(err) } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(4), Subject: csr.Subject, NotBefore: time.Now().Add(-600).UTC(), NotAfter: time.Now().AddDate(1, 0, 0).UTC(), SubjectKeyId: id, ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageAny, x509.ExtKeyUsageClientAuth, }, } // sign the CSR creating a DER encoded cert crtBytes, err := x509.CreateCertificate(rand.Reader, tmpl, cacert, csr.PublicKey, cakey) if err != nil { t.Fatal(err) } crt, err := x509.ParseCertificate(crtBytes) if err != nil { t.Fatal(err) } certRep, err := msg.Success(cacert, cakey, crt) if err != nil { t.Fatal(err) } testParsePKIMessage(t, certRep.Raw) } func TestNewCSRRequest(t *testing.T) { for _, test := range []struct { testName string keyUsage x509.KeyUsage certsSelectorFunc scep.CertsSelectorFunc shouldCreateCSR bool }{ { "KeyEncipherment not set with NOP certificates selector", x509.KeyUsageCertSign, scep.NopCertsSelector(), true, }, { "KeyEncipherment is set with NOP certificates selector", x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment, scep.NopCertsSelector(), true, }, { "KeyEncipherment not set with Encipherment certificates selector", x509.KeyUsageCertSign, scep.EnciphermentCertsSelector(), false, }, { "KeyEncipherment is set with Encipherment certificates selector", x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment, scep.EnciphermentCertsSelector(), true, }, } { test := test t.Run(test.testName, func(t *testing.T) { t.Parallel() key, err := newRSAKey(2048) if err != nil { t.Fatal(err) } derBytes, err := newCSR(key, "john.doe@example.com", "US", "com.apple.scep.2379B935-294B-4AF1-A213-9BD44A2C6688") if err != nil { t.Fatal(err) } csr, err := x509.ParseCertificateRequest(derBytes) if err != nil { t.Fatal(err) } clientcert, clientkey := loadClientCredentials(t) cacert, cakey := createCaCertWithKeyUsage(t, test.keyUsage) tmpl := &scep.PKIMessage{ MessageType: scep.PKCSReq, Recipients: []*x509.Certificate{cacert}, SignerCert: clientcert, SignerKey: clientkey, } pkcsreq, err := scep.NewCSRRequest(csr, tmpl, scep.WithCertsSelector(test.certsSelectorFunc)) if test.shouldCreateCSR && err != nil { t.Fatalf("keyUsage: %d, failed creating a CSR request: %v", test.keyUsage, err) } if !test.shouldCreateCSR && err == nil { t.Fatalf("keyUsage: %d, shouldn't have created a CSR: %v", test.keyUsage, err) } if !test.shouldCreateCSR { return } msg := testParsePKIMessage(t, pkcsreq.Raw) err = msg.DecryptPKIEnvelope(cacert, cakey) if err != nil { t.Fatal(err) } }) } } // create a new RSA private key func newRSAKey(bits int) (*rsa.PrivateKey, error) { private, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { return nil, err } return private, nil } // create a CSR using the same parameters as Keychain Access would produce func newCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error) { subj := pkix.Name{ Country: []string{country}, CommonName: cname, ExtraNames: []pkix.AttributeTypeAndValue{{ Type: []int{1, 2, 840, 113549, 1, 9, 1}, Value: email, }}, } template := &x509.CertificateRequest{ Subject: subj, } return x509.CreateCertificateRequest(rand.Reader, template, priv) } func loadTestFile(t *testing.T, path string) []byte { data, err := ioutil.ReadFile(path) if err != nil { t.Fatal(err) } return data } // createCaCertWithKeyUsage generates a CA key and certificate with keyUsage. func createCaCertWithKeyUsage(t *testing.T, keyUsage x509.KeyUsage) (*x509.Certificate, *rsa.PrivateKey) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } subject := pkix.Name{ Country: []string{"US"}, Organization: []string{"MICROMDM"}, CommonName: "MICROMDM SCEP CA", } subjectKeyID, err := cryptoutil.GenerateSubjectKeyID(&key.PublicKey) if err != nil { t.Fatal(err) } authTemplate := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: subject, NotBefore: time.Now().Add(-600).UTC(), NotAfter: time.Now().AddDate(1, 0, 0).UTC(), KeyUsage: keyUsage, IsCA: true, SubjectKeyId: subjectKeyID, } crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, &key.PublicKey, key) if err != nil { t.Fatal(err) } cert, err := x509.ParseCertificate(crtBytes) if err != nil { t.Fatal(err) } return cert, key } func loadCACredentials(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) { cert, err := loadCertFromFile("testdata/testca/ca.crt") if err != nil { t.Fatal(err) } key, err := loadKeyFromFile("testdata/testca/ca.key") if err != nil { t.Fatal(err) } return cert, key } func loadClientCredentials(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) { cert, err := loadCertFromFile("testdata/testclient/client.pem") if err != nil { t.Fatal(err) } key, err := loadKeyFromFile("testdata/testclient/client.key") if err != nil { t.Fatal(err) } return cert, key } const ( rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" certificatePEMBlockType = "CERTIFICATE" ) func loadCertFromFile(path string) (*x509.Certificate, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode failed") } if pemBlock.Type != certificatePEMBlockType { return nil, errors.New("unmatched type or headers") } return x509.ParseCertificate(pemBlock.Bytes) } // load an encrypted private key from disk func loadKeyFromFile(path string) (*rsa.PrivateKey, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode failed") } if pemBlock.Type != rsaPrivateKeyPEMBlockType { return nil, errors.New("unmatched type or headers") } // testca key has a password if len(pemBlock.Headers) > 0 { password := []byte("") b, err := x509.DecryptPEMBlock(pemBlock, password) if err != nil { return nil, err } return x509.ParsePKCS1PrivateKey(b) } return x509.ParsePKCS1PrivateKey(pemBlock.Bytes) } func decodePEMCert(t *testing.T, data []byte) *x509.Certificate { pemBlock, _ := pem.Decode(data) if pemBlock == nil { t.Fatal("PEM decode failed") } if pemBlock.Type != certificatePEMBlockType { t.Fatal("unmatched type or headers") } cert, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { t.Fatal(err) } return cert } golang-github-micromdm-scep-2.0.0/scep/testdata/000077500000000000000000000000001407235022200215355ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/scep/testdata/CertRep.der000077500000000000000000000110151407235022200235760ustar00rootroot000000000000000  *H 01 0+0 *H 0 *H {0w1F0B0,0'10U MDM SCEP SIGNER1 0 UUS0  *H _ʭ%b v"-- 6|XuI%-oqWtA~"Bk.<щי&,lKܶ/eثg\_QLO}Mzk0NvVc-{t2\'UM^a9C!ơPT{mČB/-bd Fps =y12X0'>GSS gsh;ͳwߓ]~5aG5ҷU n$0& *H 0+d 2VkA DwHKeHSUk5la0CXsjqP™ k$&s{܄u^3$<(DIކ ?/J Anc:eسp߲=*RO1 *|s9j4]eXbϼth(x ]NG5dlQdC/pPdUߠ 直Hkn1cQi"E'l\oIw趒]Y zFfδfhAoe;u;ksl{ ԑeq)Z*5K2I-M!eLA4'iI3+m6/pQ,]EjWjxos_ŋ')t ]HF(Rl:# M?%iO714 (g֛$BFקOVy{FR)&`K;~[XtEQ sjH݉TUzl+quq1,|0.W{)\&Sj\D)4Zcrs}#PfS7ɞ{(s߸*wMd`mD`]\ L.YV\mu{ydP4kCL4oq@RmɿulNM%S%~-ԅ~#e()[ATH ΠWa{$r 000  *H  0-1 0 UUSA10U etcd-ca1 0 U CA0 160531113014Z 170531113014Z0G1E0CU;BVlD^&-+t]R[ˌ>alѱkdC)uW-k_q7]0[0U%0U%+0U0v0fd]7C= e+0U#0\?{Dwu榓0  *H  %NU0~[ I1W o N/ 5%ʘmg'х? c#!Q@h5hm9Cz@Z/w1QJс/n9mD1#geKd?(tOڪv]oLҎ=Z<˖h $REkUD?xrE'Q7 }<˔Sfy-#-` &yF9eu'FȺαvJvpD-ɠ'q]}KNۅPI˖+Prt 2s}v[sGCݧ=hְj|^]$M@ :OZ|Z*_O(pTf)N$ 62:v9 \,)4QhEd|s,DBp?94 :)b "Z\XP;080 0  *H  0-1 0 UUSA10U etcd-ca1 0 U CA0 160529134705Z 260529134708Z0-1 0 UUSA10U etcd-ca1 0 U CA0"0  *H 0 KκQyZ{|Us=ͣUYwXݲ6nl':L>{پ͑c>{y\Zd>B}-[GFh0~wĆDFYrCі9֑JQK҈k s\vp(gAc E*îtS,y ${L̙!-oc r*ѤpsѹF5w1L lD_fBLL+[o\ϓR@(WIsېU;9.&K 2q_*gS{|DT9'S\i\Bi{܊BX|zO tXp9j(Qo?A<]L9[>_jrfM4|hVS8.7.Ã$aԲqqLh>zĀ2jĈA5ݶeޟ2K?Io.fF1uu(e.v㗟c0a0U0U00U\?{Dwu榓0U#0\?{Dwu榓0  *H   D:P_r 3Cӕ3m-4i4/V 080Б 1"rp^c6G1uk̰)׻4Obd {SfҒnAc_Q'MdWU+Jgc7<GRbq.k]W 9إCQW*RLE-V~毳zs(4 %+%USIwaIvB\vdz6s˺"Q\帏 C+%g4'-$kSk&eL7 ]^LNXn wH":=4kQ5MS]0Ao{U&ũΚ^[a-ۢ9GoL'綢2Vma!ye:KxjxTX!”,`SFnm[̦q oxNnU,8 _ox?S(1_*p @܌|m%Qr!v$3Δpg!j»}Ls;nV,ZwĥH_[(FߨNCa(]:BI_)^/0b-/2:+o BuH6?\uZ7fP]ޱ048m(;e޸E՝^y_`<#7W|kر;7MH̡q:eY̑# \HQo|H/G?INqVdiؚ*KTܙ9=qNM$Z3pwͺSxtz0c5QM5+{ju5Kɢ:ɨNv9M%/4O2:4#6ϽRC;O} ZIXA޳M1e2h_\ø:5TwKPUV)[3 wO7mCm*UMZ -V"Vm] 0 *H 0*H ݖ2Ea tq&y.^9sOJBLh} ܷ!zf4!]U1rDE.͗qjɅ&<[gfx^s}oKsQ=1׷u7Q Yyu&.? ٰ6Zl\I"VB.M!6k׻Ҝ^>b(7 >G,Aioe0EMB4?-O][ 焴={N\BM.QMyQj$*wɖIjc`pJP.IKK̴dP)#ƌpz'y Mo9( c7Æ 9B+U-85aJbx`V@%l"qڒn$vy(ǂXeޙ-00۠0  *H  0'10U MDM SCEP SIGNER1 0 UUS0 160531113013Z 170531113013Z0'10U MDM SCEP SIGNER1 0 UUS0"0  *H 0 wQCW{6 uYNmv\k0_:]Dmխ~I ,&Vŋ2B?a33kC]BD[W9vxdOOSz5.I}LX!r<[u WM8j#|W~3^@b$,L`  XqÉfQR:hgU vDFa`N;zg}[ÀL1[ɘY6*0(0U0U% 0 +0  *H  heUQ8E@jgV_cϙ^DtL=䵜H/PH 鬊9Tp(Rk1AJnWz i[(:6XK;wsc"Vįj}L=#>専O1䄅[ofpEP100,0'10U MDM SCEP SIGNER1 0 UUS0 +0 `HE 1190 *H  1  *H 0 `HE 1 lƾ0 *H  1 160531113013Z0# *H  1H^4m u]jo (08 `HE 1*(A13090761A30F66664F85D37433D2065E1112BA10  *H \v`,9uUt"JeBy [VFoWC[>zGz65!ULӀ4q"DEG1+5;[*@P_1 ~ E虾ӕ4LQ4zߐTvĵRl Z=TdW'q98t$>4Q ןx;ߵDeJ \]0v6Dq2Q as6'fpKekHj7T~+k%Ƕ4golang-github-micromdm-scep-2.0.0/scep/testdata/testca/000077500000000000000000000000001407235022200230205ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/scep/testdata/testca/ca.crt000066400000000000000000000035161407235022200241220ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAtMQwwCgYDVQQGEwNVU0Ex EDAOBgNVBAoTB2V0Y2QtY2ExCzAJBgNVBAsTAkNBMB4XDTE2MDUyOTEzNDcwNVoX DTI2MDUyOTEzNDcwOFowLTEMMAoGA1UEBhMDVVNBMRAwDgYDVQQKEwdldGNkLWNh MQswCQYDVQQLEwJDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALEG S866Uf79znmx8+BakJ17tox8VYem0NZzPc2jF4RVWXfT481Yz9jdsjZubMCFuJiI JzpMBT7RzXvZvuzMzZEe77Tb0mM+83t5kVwWWuxkEz7HQn0tWxuLR7NGaAi5MH53 pcSGRNH8RgC7WdhyQ/3HwNGWObe0wQT69tfz1pHDSvNR9v7DS9KIiGsMc+dcqayz n3YQuwEV8nD1KGenxEFjFh0NsP5FKrzDrsvzdFOWLJ3jedfDCSQSe0y33syZIYAQ wS2/b+io6GMWDQemcirN9QiI1NGkcN9zioPRuYPxkaxGNa0O+3cTgA8egTFMigvI 4ZFsmERfZkJM4sBMK1uUmxXKb87nA1zooPvPk1KGQChXBEnrkHPbkP1VO+yYOS4m t9LDweGVS6GoC5vjqQgymOHecaNfKpBnU6t7fP/aEZUF+6mxRKofolR/hTknkVNc q2nrXEJpz8J73Iq8rkL0rNAEu1h83npPAoUgdFhwHzlq9ShRbz+ZQTxdAv5MOVs+ 6F9qcmbv/6C4xc1N1xH2NAJ8aFZTxsw4ny43hi7DgyRh1LJxcb2Bp7JMaD56CMSA 0zJqxIiV5kGUwbmrBjXMyvjYzx/0qI3j3bZl3p8BjZgyjkvOP0nArP3bby5mEUYx i7+YgPm8dfGIzPh19I4oFReszOJl+JrdLnbf45efAgMBAAGjYzBhMA4GA1UdDwEB /wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT6XD/PBaV7GbFEnxOm 3OJ3deamkzAfBgNVHSMEGDAWgBT6XD/PBaV7GbFEnxOm3OJ3deamkzANBgkqhkiG 9w0BAQsFAAOCAgEAC6yBHrRElZ7ovDrqjVBf8fLG+nINETPJ/kPTlTNtvqClLaeE NKPH6JVp0/uusoKmqvE0LxyBEdP7waHQVq2XnfYggDCNjAUFxdv7OKAwlBjJ0JGs 5RsJ9DEehyLecnDDDhte92M2xUcfMet1BmuizLDDKaUU17sI1g/UNE+c7hViZA2J e+wezVOUZqCY0pICsm4ar8JBY/pfUZ+1J00AZJtXuVWqK5GYGkrLZ7ZjNzzDF0cY UmJxki5rj11XpCCQOZjVB+Pp3t7YpUOey1EC+1fKKrdS40zaRS3VVgh+Guavs5HV egBzKDQUuRrZDbodJSv28RYlVbFTmkl3hGGNE0l2v0L2XHasZHoBkDZzz9nLuiI8 ZdhWS+fn7dbswN9WzzB+dPzKS1WkTj5RXL/luI/7+fYNQyvIJYdnNCegyi2C2yTD a/vmFJkBU+uLHWsW9a8R5Ca7A91ltJobTJE3uwxdXuZMTrmlWKsEbhqHCqO7d0j8 IgYGxDo9ysfA4AOiNDxlp7lXxV/JFOsuGXNdFKcDFykLZ5u21X9ho9fptWJDP9JN NNOXjC0Jv2UGZrHze6IqyL5JqxOGpK22PQIwpZwExwijUom+LH5VEXK1zpXzwC93 WXWVtGOW4yEqv0VTn7vafIeM5GBTJ44ggpkp4RpFWoBMZcAFj8gE/9AUaHo= -----END CERTIFICATE----- golang-github-micromdm-scep-2.0.0/scep/testdata/testca/ca.crt.info000066400000000000000000000000011407235022200250360ustar00rootroot000000000000002golang-github-micromdm-scep-2.0.0/scep/testdata/testca/ca.key000066400000000000000000000063571407235022200241300ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,85186376af1462c4 jY1gAV22U2GeDZW0cVFw41uABS7fe7zKQen4aQQvkFJ5DPRlZFMI2qZ2MI1Oo265 ek1Of/pcV52ct139NuVg9JZwkPPog40xUDn72IanZ2ZvJl/dcoHFn99T816Hu0p7 YGKpvyyy3VYuxaarZV2aUNFye/o4bnAh4P0db6qa7/sGAKAhJ9PusNcSAXWWMz1l QSCkdD30KrZOtf39StVnNSf2vPWAjAR/w3fEOVKqEehANP7yptDVOthiqrN+p58Q kOGf3RnA5BJ21EY09W8rbgKE18EPI0UH9LVZEvRabZd4e/VSRNL6leNO7AiMLqA0 3P7DAvjQDeTpYu3tZNWKFlmXv28KjwNo/4mclQTM8k5nkpQcLhGEVJanMlz0NAvZ +BOgHGt1Vd8fXp5vBEMIz0tj7jJJnyEyd6tLRc7Pm7GaFjRy70rr/ZCO27HnKeWi BVwFmZqG6Bcc1WvOGH/w549q/Xq1B3EgjSCShd7WQqINXMFOLJRg+MVZ9/EgWTrT AEMkWwozb7hDJ56IdjE6PUop/5nbH87YjXHreV4kaGxgz3xD+sj6MGl8uw2JeT7b 6mFrK2d+704NU0Z56w8i0ydYeNn1uFmZqL9alYTDmAjOORXAR/ApvY6ctPXSpPTW bXgv7LNWbcD8cNWuf/24dpI+kxbrIsKdGmucjQQ066Ce9qa2Kr7HEpH/CxxKCuBx 9KTHpb2ZZI1j6Zd9DQarEQm2D9fPaqEIq9XH46tNq8twXXEaYSTwwodSkwlovB5n e4HlbiSuHB78ej2lQyFKquqWVYMRQ3dk5CUem/4XPF0L8dPvnifoQgBMpvJvzCG5 BsIDQXKf0qLhQPrXwemhgY8fnZqDpuRTD6mdEXoPqvJC5L+3hzpPXHtCU94oqIbq z4lkG1ARi9yS+WfUbXXZO95+7EBBg4lXzEZvXjqY6epUVjWCnoa873H9zfMZBuLL XkxMQyDOnXaqYeqNsCahbdH4zuobR1SCNL4nt3iSaADaN6Lezwz8LPHxoM1kG1i6 fvPa/uRo9aVsfWsovO+od2VmqLh1sfPZoOenZSKQsAVPYmEuV8XXVJ/B8NVvNTrt DrfAR+vFe41liMHdTUndo8uG9/IO7JNC8u98zWjyvcr6cCukqE9H60Y9QvDgaSK5 yD/D7B4ZAp6UjHOtD+jY1mjV+aL/2XeHJyQaDczHUKm1Vd5Um2c5f6NkZycrbtGE 7z5lR2SccnbbG6XVngYiZxdMLZCrQUnSfhke+zhzYM2Ng/7fxyz3mTyG5EYvxreU 6i0Psceh+vD9IEGHYbpRfV4Uozmk7AhEOfQN3ZDXZTA4LB5Svp7j3DcOmAGtCWGx PWA3su4KzwrW40b5ommDhndPZNoSJfsrw2GJHV62AdfIxmAi5zvALJ31YdYvZsz2 e8cf1Cl5oxeF/jgewEy6RTSkOUjvb0iTfVgreu4Tk/sBW37jdKhfW32INasCgEYb 0fq9DLXVcDk2neH/Sb78cE26JNXS3EtW1V4dvdkhvOjqRFP8O8vFggLi1mFQltAH pmV143MSNkC/ikyOBahpQjGu89HZ0sLnJr2kzKf5LJTcN7kYAfxRejS7ofUByME1 O9mrHOZGGNVNIgNesBXv42UEd0/SzwF4UKxHY72sEoTNLXliroaJORYbbvWw4GDI 91/vHKJMqMimoC37soS16wrsP/SabzusUXBayHD/PLkkmHBPV9++cO79b+HbVB0Q 6OpxBY7u6QhZnfTJv/W/InG404pumq8oz6bt7bXurbfC2QzviNHuyZ/IenbQ/y41 K5URD3fdFYLC3OS38SSBBq32yncjJam0FOj2joUZ4iAAXSju1NSDskT8WbVy3BOq tdTxekrxM9w98p17Og+Uf8966H2mQUIrz53Umc9V1974TVWdu0Y862ghJGSeLEbH 617VGwNN9hINdQE+iYaAVvbogEKSdCfljyVdIx1MuS1jeae5wUgReqqE+bopYgJm oIXlVNI7tWX2y3JdG1vqCqKpq/UDzciLxAUdyGgwZESt9T3mvqQcdvWxsfREBGwX XzbiDiGoom735dOOaGxvmyZUtJi7r5AonzJpR+qaRWoHNr8cqeU9be1wxBZ57Kln 2eKpwPIwdBTwxCjnc/kstuTsR45M8G37zOgh9XK38jS6FB/FzFytHtt9oPQBZBeb 3A6p7kqbbb4ynAgDiGEz9ExNNIQf3hQo9RAiaL2WeS9FTFB02hq5QgsgrGVXrR2V 45CKzP874sMPYP8xFQvmrMAXDy//zBXaOrNJHyNOVrtDLPerBNIC6GSKtFp30ynz Te6GHFhcwqWfrN8N1l2oM79xvc2aKlsvI+YN0xTQklxqSdyCJSdhRUxmCIMN1JM0 13Ean0HtO+z9u/nH3T2GtAhNySJAPAOXIAAER/74WNXNJNi7SmptNtWJOKKeKK3m Jon7XC3Bx5NTnTM6UjrrXvwXvsJyf8G+SlkoZXZx9izgQYAANAsSblieSvPVppwM /EfU6HIby2cBLQ1wTJiEDjYu7E1JKpAPBhqL0cN7aJea9tV7bmjoqzKhbwxACHkI ymOZ1BDIF67M5fCLFCnCZEJcl2sgx4bRBaP6+p0uRWhplrus+8x1LAtNyB+V17in nXacPqGELgqv+F6embq03retfaCbIwLwQYmaMU+QHg9jHc9j1AIf6fHSxhIRUPUz PWMhy7dJdUcmm2GX2EGBrr7jH+H2y33W7y+0I2a4s5WdpIWsYUMFiBU+M+qJdAwY O/n1Q8ZPdKdY9+c2RMzeO6Zvyc7f1hwoOy0FuYi748qaELV6rx1Tr2MDWl5/uhUa vYMF4RshsKJY9OCUKvL9waqELZf4zEPyu875ZLm9eoJV2MFcokUuPcpAN+ljj6mx S+1O9/kRioHo7FMs9rU3bHbCMbphLc0NdI363L/sM2kSFjRWxYv87z5fEQAoZGQR d7HePVRbp09GC9Jk9p28F6ysgqS7PwlreRRp3Dj5vFJ422QviUWTP/jLj1QfukQR 0KXZhKhs0iSmfW9vlFnADS32l67fmycHMlN9yktvzcytm6dZ/XiQMHVDhZPlIGVC frJ2R1MhmAdFEgIPZZGuoHeXFdlYq9HMpM9lbykJ1L7M36XqaW6GgRTnhf2g4iKJ -----END RSA PRIVATE KEY----- golang-github-micromdm-scep-2.0.0/scep/testdata/testca/ca.pem000066400000000000000000000035161407235022200241130ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAtMQwwCgYDVQQGEwNVU0Ex EDAOBgNVBAoTB2V0Y2QtY2ExCzAJBgNVBAsTAkNBMB4XDTE2MDUyOTEzNDcwNVoX DTI2MDUyOTEzNDcwOFowLTEMMAoGA1UEBhMDVVNBMRAwDgYDVQQKEwdldGNkLWNh MQswCQYDVQQLEwJDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALEG S866Uf79znmx8+BakJ17tox8VYem0NZzPc2jF4RVWXfT481Yz9jdsjZubMCFuJiI JzpMBT7RzXvZvuzMzZEe77Tb0mM+83t5kVwWWuxkEz7HQn0tWxuLR7NGaAi5MH53 pcSGRNH8RgC7WdhyQ/3HwNGWObe0wQT69tfz1pHDSvNR9v7DS9KIiGsMc+dcqayz n3YQuwEV8nD1KGenxEFjFh0NsP5FKrzDrsvzdFOWLJ3jedfDCSQSe0y33syZIYAQ wS2/b+io6GMWDQemcirN9QiI1NGkcN9zioPRuYPxkaxGNa0O+3cTgA8egTFMigvI 4ZFsmERfZkJM4sBMK1uUmxXKb87nA1zooPvPk1KGQChXBEnrkHPbkP1VO+yYOS4m t9LDweGVS6GoC5vjqQgymOHecaNfKpBnU6t7fP/aEZUF+6mxRKofolR/hTknkVNc q2nrXEJpz8J73Iq8rkL0rNAEu1h83npPAoUgdFhwHzlq9ShRbz+ZQTxdAv5MOVs+ 6F9qcmbv/6C4xc1N1xH2NAJ8aFZTxsw4ny43hi7DgyRh1LJxcb2Bp7JMaD56CMSA 0zJqxIiV5kGUwbmrBjXMyvjYzx/0qI3j3bZl3p8BjZgyjkvOP0nArP3bby5mEUYx i7+YgPm8dfGIzPh19I4oFReszOJl+JrdLnbf45efAgMBAAGjYzBhMA4GA1UdDwEB /wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT6XD/PBaV7GbFEnxOm 3OJ3deamkzAfBgNVHSMEGDAWgBT6XD/PBaV7GbFEnxOm3OJ3deamkzANBgkqhkiG 9w0BAQsFAAOCAgEAC6yBHrRElZ7ovDrqjVBf8fLG+nINETPJ/kPTlTNtvqClLaeE NKPH6JVp0/uusoKmqvE0LxyBEdP7waHQVq2XnfYggDCNjAUFxdv7OKAwlBjJ0JGs 5RsJ9DEehyLecnDDDhte92M2xUcfMet1BmuizLDDKaUU17sI1g/UNE+c7hViZA2J e+wezVOUZqCY0pICsm4ar8JBY/pfUZ+1J00AZJtXuVWqK5GYGkrLZ7ZjNzzDF0cY UmJxki5rj11XpCCQOZjVB+Pp3t7YpUOey1EC+1fKKrdS40zaRS3VVgh+Guavs5HV egBzKDQUuRrZDbodJSv28RYlVbFTmkl3hGGNE0l2v0L2XHasZHoBkDZzz9nLuiI8 ZdhWS+fn7dbswN9WzzB+dPzKS1WkTj5RXL/luI/7+fYNQyvIJYdnNCegyi2C2yTD a/vmFJkBU+uLHWsW9a8R5Ca7A91ltJobTJE3uwxdXuZMTrmlWKsEbhqHCqO7d0j8 IgYGxDo9ysfA4AOiNDxlp7lXxV/JFOsuGXNdFKcDFykLZ5u21X9ho9fptWJDP9JN NNOXjC0Jv2UGZrHze6IqyL5JqxOGpK22PQIwpZwExwijUom+LH5VEXK1zpXzwC93 WXWVtGOW4yEqv0VTn7vafIeM5GBTJ44ggpkp4RpFWoBMZcAFj8gE/9AUaHo= -----END CERTIFICATE----- golang-github-micromdm-scep-2.0.0/scep/testdata/testca/sceptest.mobileconfig000066400000000000000000000111311407235022200272260ustar00rootroot00000000000000 PayloadContent PayloadContent KeyType RSA Keysize 1024 Retries 3 RetryDelay 10 URL http://localhost:9001/scep PayloadDescription Configures SCEP settings PayloadDisplayName SCEP PayloadIdentifier com.apple.security.scep.063D7953-1338-4BF0-8F99-913382996224 PayloadType com.apple.security.scep PayloadUUID 063D7953-1338-4BF0-8F99-913382996224 PayloadVersion 1 PayloadCertificateFileName ca.crt PayloadContent LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZPRENDQXlD Z0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREF0TVF3d0Nn WURWUVFHRXdOVlUwRXgKRURBT0JnTlZCQW9UQjJWMFkyUXRZMkV4 Q3pBSkJnTlZCQXNUQWtOQk1CNFhEVEUyTURVeU9URXpORGN3TlZv WApEVEkyTURVeU9URXpORGN3T0Zvd0xURU1NQW9HQTFVRUJoTURW Vk5CTVJBd0RnWURWUVFLRXdkbGRHTmtMV05oCk1Rc3dDUVlEVlFR TEV3SkRRVENDQWlJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFE Q0NBZ29DZ2dJQkFMRUcKUzg2NlVmNzl6bm14OCtCYWtKMTd0b3g4 VlllbTBOWnpQYzJqRjRSVldYZlQ0ODFZejlqZHNqWnViTUNGdUpp SQpKenBNQlQ3UnpYdlp2dXpNelpFZTc3VGIwbU0rODN0NWtWd1dX dXhrRXo3SFFuMHRXeHVMUjdOR2FBaTVNSDUzCnBjU0dSTkg4UmdD N1dkaHlRLzNId05HV09iZTB3UVQ2OXRmejFwSERTdk5SOXY3RFM5 S0lpR3NNYytkY3FheXoKbjNZUXV3RVY4bkQxS0dlbnhFRmpGaDBO c1A1RktyekRyc3Z6ZEZPV0xKM2plZGZEQ1NRU2UweTMzc3laSVlB UQp3UzIvYitpbzZHTVdEUWVtY2lyTjlRaUkxTkdrY045emlvUFJ1 WVB4a2F4R05hME8rM2NUZ0E4ZWdURk1pZ3ZJCjRaRnNtRVJmWmtK TTRzQk1LMXVVbXhYS2I4N25BMXpvb1B2UGsxS0dRQ2hYQkVucmtI UGJrUDFWTyt5WU9TNG0KdDlMRHdlR1ZTNkdvQzV2anFRZ3ltT0hl Y2FOZktwQm5VNnQ3ZlAvYUVaVUYrNm14UktvZm9sUi9oVGtua1ZO YwpxMm5yWEVKcHo4SjczSXE4cmtMMHJOQUV1MWg4M25wUEFvVWdk Rmh3SHpscTlTaFJieitaUVR4ZEF2NU1PVnMrCjZGOXFjbWJ2LzZD NHhjMU4xeEgyTkFKOGFGWlR4c3c0bnk0M2hpN0RneVJoMUxKeGNi MkJwN0pNYUQ1NkNNU0EKMHpKcXhJaVY1a0dVd2JtckJqWE15dmpZ engvMHFJM2ozYlpsM3A4QmpaZ3lqa3ZPUDBuQXJQM2JieTVtRVVZ eAppNytZZ1BtOGRmR0l6UGgxOUk0b0ZSZXN6T0psK0pyZExuYmY0 NWVmQWdNQkFBR2pZekJoTUE0R0ExVWREd0VCCi93UUVBd0lDQkRB UEJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJUNlhE L1BCYVY3R2JGRW54T20KM09KM2RlYW1rekFmQmdOVkhTTUVHREFX Z0JUNlhEL1BCYVY3R2JGRW54T20zT0ozZGVhbWt6QU5CZ2txaGtp Rwo5dzBCQVFzRkFBT0NBZ0VBQzZ5QkhyUkVsWjdvdkRycWpWQmY4 ZkxHK25JTkVUUEova1BUbFROdHZxQ2xMYWVFCk5LUEg2SlZwMC91 dXNvS21xdkUwTHh5QkVkUDd3YUhRVnEyWG5mWWdnRENOakFVRnhk djdPS0F3bEJqSjBKR3MKNVJzSjlERWVoeUxlY25ERERodGU5Mk0y eFVjZk1ldDFCbXVpekxEREthVVUxN3NJMWcvVU5FK2M3aFZpWkEy SgplK3dlelZPVVpxQ1kwcElDc200YXI4SkJZL3BmVVorMUowMEFa SnRYdVZXcUs1R1lHa3JMWjdaak56ekRGMGNZClVtSnhraTVyajEx WHBDQ1FPWmpWQitQcDN0N1lwVU9leTFFQysxZktLcmRTNDB6YVJT M1ZWZ2grR3VhdnM1SFYKZWdCektEUVV1UnJaRGJvZEpTdjI4Ulls VmJGVG1rbDNoR0dORTBsMnYwTDJYSGFzWkhvQmtEWnp6OW5MdWlJ OApaZGhXUytmbjdkYnN3TjlXenpCK2RQektTMVdrVGo1UlhML2x1 SS83K2ZZTlF5dklKWWRuTkNlZ3lpMkMyeVRECmEvdm1GSmtCVSt1 TEhXc1c5YThSNUNhN0E5MWx0Sm9iVEpFM3V3eGRYdVpNVHJtbFdL c0ViaHFIQ3FPN2QwajgKSWdZR3hEbzl5c2ZBNEFPaU5EeGxwN2xY eFYvSkZPc3VHWE5kRktjREZ5a0xaNXUyMVg5aG85ZnB0V0pEUDlK TgpOTk9YakMwSnYyVUdackh6ZTZJcXlMNUpxeE9HcEsyMlBRSXdw WndFeHdpalVvbStMSDVWRVhLMXpwWHp3QzkzCldYV1Z0R09XNHlF cXYwVlRuN3ZhZkllTTVHQlRKNDRnZ3BrcDRScEZXb0JNWmNBRmo4 Z0UvOUFVYUhvPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== PayloadDescription Configures certificate settings. PayloadDisplayName ca.crt PayloadIdentifier com.apple.security.root.2E5B325F-84CA-4914-844F-703F9C4B11CE PayloadType com.apple.security.root PayloadUUID 2E5B325F-84CA-4914-844F-703F9C4B11CE PayloadVersion 1 PayloadDisplayName scept PayloadIdentifier vvrantchmbp.local.A795EFE8-60CA-47DA-92F3-FE2E435D800F PayloadRemovalDisallowed PayloadType Configuration PayloadUUID 7F0CF6ED-09FF-490E-AD53-89ACB920CD37 PayloadVersion 1 golang-github-micromdm-scep-2.0.0/scep/testdata/testca2/000077500000000000000000000000001407235022200231025ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/scep/testdata/testca2/CertRep_NoCertificatesForSigners.der000066400000000000000000000052541407235022200321340ustar00rootroot000000000000000  *H  0 1 0 +0 *H 0 *H 01^0Z0B0.10U  scep-clients410U SCEP SIGNERO EYO$X׾\0  *H 0ZW4]1<̞[\mKH!Gnk!94id؛X|`>Jm}w:KFu6PgPr()\OnF_ as, Mf>;6 DJ-]b}UfDŮ#${1ݳOpt*!.XCwM2C»q.fE‘Piudz?so5OhbW0n *H 0 `HesUkd+ƀ@~ W4 &ӷz]]R%G!/ W7ҖX"dKKLC{9?<&3>BaF75R.D:Ȗڳ#aB^]qN6fRg`}DcotWR5ߧ ЎְUCQER%+`o30Ѧ:t@w NyqKIIW8ޜGI8R ?iBHkY#A .{!у %:ʼ(ͷ,Zv{R" sJG.هĘdOȝDYVz--۳6R6BBWPuZaqhac@N R}B.AگѦ?#s_S_j-o&5x-ݝABpvm;ScGrRQRd1<[Wenl[ q7vH X 5fyK pEdEB #}W .;"3P|Zu׿ABI3$S^^4l"Tp^ w LldcmHď3 #O rErwRMLzZIpv@G9PKQ#ibI30@U[gԕx(H8ʙQ["C " >xqI ƌˍYmFဵY@U8r/vLƉIytls2nO"ŤXj~y&B2&rK@СIgu؞_^ha 6   Kp&8eJ0!:I6EDud#̰+%&йϸhZ= 4D.8Sע= a22w Cбga.1gGS_J‚⩢t%FtV,.ev\LGg'j|*^S|ag̤db< kHpHEW͈˿2".;2PЂǐTgm̓1 3wlx2E͓-jV;yolA@W e 4X^'PBWd?A>8 K 1(x$iez8OK,RQ6|cFN7%FZ,YEdkM!~I,UI{YiŌʓ>+_tĿ{a߲QyY2_sc"kkEm95] 5Od،jW1|0x0d0M10 &,dorg10 &,dexample10Uexample-CERT-PROV-CAG@ u݇0 +0 `HE 130 `HE 100 +7&1 Issued0 *H  1  *H 0  `HE 1E]ŏ~[D*P0  `HE 1ٝ1T@Y\\s0# *H  1i: K^0, `HE 1HVvzN0228V+AW+dUuDe8OPyDXD0=0  *H _ͤHc[8a͠[N4&@RpZ VtyQ_5vրʉ{]*0!-XoH<ٽ@\Jw$0#@Wz@Erۢ_=tnc */1IKTCuIUjp#% -----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDT9YGr0H8dpozAEi5l2XkWyKy2JD3yEybI9A1ZDXcK/78UPQ+C 4tBb6BTRJWDWoZFlFcHUGbZWXbySPw6ggBsLl4feF1A+hjtCjlZsRF4mnfctixkr dP+UGl37UunsW63mn8uM6oM+7elhB2zRscZrZPBDKZx1V+Et+BFrX49xNwIDAQAB AoGAP9bzDmfG0YxnWjZfqSd+NCGO+3EhAzdHeEEhgA/xKevrhlH5yQc9kGDvXCrw 5tRU8WhDL/nqlEq5UCcT5b2P5zp1L0PY+X6gD5C7KGEIio5SvimAnQMh2HCKftDc KX9NA9EJLq9BUsqK9HjXdsfIGzyoqhZQpDGTrgyVlfm/zwkCQQDyuKJvxm/6tWry GBN0eBLyA4F738MFP/3kb87zUsGTD8dh92vwjMhGv1woKp09POs2s2MnADw2wOEa hh+v6R5zAkEA344KSwaKZZSf7E5qFayg3qG9M55uhbus9CazoAWiceYMR6ImIevA EtnhwQczIRGI8Bp4jgUtO9gu4IgjCUHNLQJBAIJkS+cuPGP75+sMog70noDi/0GT 0MnWOcfphMzUzWb6mAr6B0Of7cuL668sTXJjcpzdO8vs5WworAU6vnUbEB8CQQCB +Hy3fcf8otoPcs9uZnzosrPjTNsI2UIGeHG6OUxmV88P3o+47O0wiIgdx2fMc/tf TKSGPTA9OMSYOc3U1fLJAkEArLyCHxxDzcEuhyHnmoct/dTUg5Q8CYKIQfo5oVKZ jvtL/r0udFpxLUDxZ7590I32cTSrtfgNBJegHr54YKN9KA== -----END RSA PRIVATE KEY----- golang-github-micromdm-scep-2.0.0/scep/testdata/testclient/client.pem000066400000000000000000000056531407235022200257050ustar00rootroot00000000000000Certificate: Data: Version: 3 (0x2) Serial Number: 6f:4f:31:6a:b2:da:d4:ce:d0:fc:09:fb:b9:26:90:03:d6:09:a4:8c Signature Algorithm: sha256WithRSAEncryption Issuer: C = US, ST = USA, O = etcd-ca, OU = CA, CN = etcd-ca Validity Not Before: Feb 16 12:11:45 2021 GMT Not After : Dec 26 12:11:45 2030 GMT Subject: C = US, ST = USA, O = etcd-ca, OU = CA, CN = etcd-ca Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (1024 bit) Modulus: 00:d3:f5:81:ab:d0:7f:1d:a6:8c:c0:12:2e:65:d9: 79:16:c8:ac:b6:24:3d:f2:13:26:c8:f4:0d:59:0d: 77:0a:ff:bf:14:3d:0f:82:e2:d0:5b:e8:14:d1:25: 60:d6:a1:91:65:15:c1:d4:19:b6:56:5d:bc:92:3f: 0e:a0:80:1b:0b:97:87:de:17:50:3e:86:3b:42:8e: 56:6c:44:5e:26:9d:f7:2d:8b:19:2b:74:ff:94:1a: 5d:fb:52:e9:ec:5b:ad:e6:9f:cb:8c:ea:83:3e:ed: e9:61:07:6c:d1:b1:c6:6b:64:f0:43:29:9c:75:57: e1:2d:f8:11:6b:5f:8f:71:37 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Key Identifier: A1:30:90:76:1A:30:F6:66:64:F8:5D:37:43:3D:20:65:E1:11:2B:A1 X509v3 Authority Key Identifier: keyid:A1:30:90:76:1A:30:F6:66:64:F8:5D:37:43:3D:20:65:E1:11:2B:A1 X509v3 Basic Constraints: critical CA:TRUE Signature Algorithm: sha256WithRSAEncryption 96:f0:7b:28:3b:7a:06:2e:cd:37:23:19:f3:98:0c:a2:d3:16: 9e:5a:b7:56:ca:9d:9d:ca:a4:59:78:b3:29:b1:3c:18:e8:dc: 4c:f6:64:62:84:a3:19:ca:ca:b0:34:ed:d2:6f:9b:a6:38:20: 98:64:db:c5:cb:a4:ce:b2:9c:62:a2:0e:e2:76:cb:f4:a1:c5: 40:ee:c5:b4:18:9d:9e:5a:bf:bd:72:29:96:f8:82:05:87:d3: fb:84:12:91:ea:e0:86:02:b1:63:c2:59:6a:10:9a:b7:7d:e2: be:f3:19:31:31:3e:bb:21:4d:a0:16:f9:c0:94:ba:0f:e6:3d: 37:26 -----BEGIN CERTIFICATE----- MIICdDCCAd2gAwIBAgIUb08xarLa1M7Q/An7uSaQA9YJpIwwDQYJKoZIhvcNAQEL BQAwTDELMAkGA1UEBhMCVVMxDDAKBgNVBAgMA1VTQTEQMA4GA1UECgwHZXRjZC1j YTELMAkGA1UECwwCQ0ExEDAOBgNVBAMMB2V0Y2QtY2EwHhcNMjEwMjE2MTIxMTQ1 WhcNMzAxMjI2MTIxMTQ1WjBMMQswCQYDVQQGEwJVUzEMMAoGA1UECAwDVVNBMRAw DgYDVQQKDAdldGNkLWNhMQswCQYDVQQLDAJDQTEQMA4GA1UEAwwHZXRjZC1jYTCB nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0/WBq9B/HaaMwBIuZdl5FsistiQ9 8hMmyPQNWQ13Cv+/FD0PguLQW+gU0SVg1qGRZRXB1Bm2Vl28kj8OoIAbC5eH3hdQ PoY7Qo5WbEReJp33LYsZK3T/lBpd+1Lp7Fut5p/LjOqDPu3pYQds0bHGa2TwQymc dVfhLfgRa1+PcTcCAwEAAaNTMFEwHQYDVR0OBBYEFKEwkHYaMPZmZPhdN0M9IGXh ESuhMB8GA1UdIwQYMBaAFKEwkHYaMPZmZPhdN0M9IGXhESuhMA8GA1UdEwEB/wQF MAMBAf8wDQYJKoZIhvcNAQELBQADgYEAlvB7KDt6Bi7NNyMZ85gMotMWnlq3Vsqd ncqkWXizKbE8GOjcTPZkYoSjGcrKsDTt0m+bpjggmGTbxcukzrKcYqIO4nbL9KHF QO7FtBidnlq/vXIplviCBYfT+4QSkerghgKxY8JZahCat33ivvMZMTE+uyFNoBb5 wJS6D+Y9NyY= -----END CERTIFICATE----- golang-github-micromdm-scep-2.0.0/server/000077500000000000000000000000001407235022200203005ustar00rootroot00000000000000golang-github-micromdm-scep-2.0.0/server/csrsigner.go000066400000000000000000000023321407235022200226260ustar00rootroot00000000000000package scepserver import ( "crypto/subtle" "crypto/x509" "errors" "github.com/micromdm/scep/v2/scep" ) // CSRSigner is a handler for CSR signing by the CA/RA // // SignCSR should take the CSR in the CSRReqMessage and return a // Certificate signed by the CA. type CSRSigner interface { SignCSR(*scep.CSRReqMessage) (*x509.Certificate, error) } // CSRSignerFunc is an adapter for CSR signing by the CA/RA type CSRSignerFunc func(*scep.CSRReqMessage) (*x509.Certificate, error) // SignCSR calls f(m) func (f CSRSignerFunc) SignCSR(m *scep.CSRReqMessage) (*x509.Certificate, error) { return f(m) } // NopCSRSigner does nothing func NopCSRSigner() CSRSignerFunc { return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { return nil, nil } } // ChallengeMiddleware wraps next in a CSRSigner that validates the challenge from the CSR func ChallengeMiddleware(challenge string, next CSRSigner) CSRSignerFunc { challengeBytes := []byte(challenge) return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { // TODO: compare challenge only for PKCSReq? if subtle.ConstantTimeCompare(challengeBytes, []byte(m.ChallengePassword)) != 1 { return nil, errors.New("invalid challenge") } return next.SignCSR(m) } } golang-github-micromdm-scep-2.0.0/server/csrsigner_test.go000066400000000000000000000007351407235022200236720ustar00rootroot00000000000000package scepserver import ( "testing" "github.com/micromdm/scep/v2/scep" ) func TestChallengeMiddleware(t *testing.T) { testPW := "RIGHT" signer := ChallengeMiddleware(testPW, NopCSRSigner()) csrReq := &scep.CSRReqMessage{ChallengePassword: testPW} _, err := signer.SignCSR(csrReq) if err != nil { t.Error(err) } csrReq.ChallengePassword = "WRONG" _, err = signer.SignCSR(csrReq) if err == nil { t.Error("invalid challenge should generate an error") } } golang-github-micromdm-scep-2.0.0/server/endpoint.go000066400000000000000000000113551407235022200224540ustar00rootroot00000000000000package scepserver import ( "bytes" "context" "net/url" "strings" "sync" "time" "github.com/go-kit/kit/endpoint" "github.com/go-kit/kit/log" httptransport "github.com/go-kit/kit/transport/http" "github.com/pkg/errors" ) // possible SCEP operations const ( getCACaps = "GetCACaps" getCACert = "GetCACert" pkiOperation = "PKIOperation" getNextCACert = "GetNextCACert" ) type Endpoints struct { GetEndpoint endpoint.Endpoint PostEndpoint endpoint.Endpoint mtx sync.RWMutex capabilities []byte } func (e *Endpoints) GetCACaps(ctx context.Context) ([]byte, error) { request := SCEPRequest{Operation: getCACaps} response, err := e.GetEndpoint(ctx, request) if err != nil { return nil, err } resp := response.(SCEPResponse) e.mtx.Lock() e.capabilities = resp.Data e.mtx.Unlock() return resp.Data, resp.Err } func (e *Endpoints) Supports(cap string) bool { e.mtx.RLock() defer e.mtx.RUnlock() if len(e.capabilities) == 0 { e.mtx.RUnlock() e.GetCACaps(context.Background()) e.mtx.RLock() } return bytes.Contains(e.capabilities, []byte(cap)) } func (e *Endpoints) GetCACert(ctx context.Context, message string) ([]byte, int, error) { request := SCEPRequest{Operation: getCACert, Message: []byte(message)} response, err := e.GetEndpoint(ctx, request) if err != nil { return nil, 0, err } resp := response.(SCEPResponse) return resp.Data, resp.CACertNum, resp.Err } func (e *Endpoints) PKIOperation(ctx context.Context, msg []byte) ([]byte, error) { var ee endpoint.Endpoint if e.Supports("POSTPKIOperation") || e.Supports("SCEPStandard") { ee = e.PostEndpoint } else { ee = e.GetEndpoint } request := SCEPRequest{Operation: pkiOperation, Message: msg} response, err := ee(ctx, request) if err != nil { return nil, err } resp := response.(SCEPResponse) return resp.Data, resp.Err } func (e *Endpoints) GetNextCACert(ctx context.Context) ([]byte, error) { var request SCEPRequest response, err := e.GetEndpoint(ctx, request) if err != nil { return nil, err } resp := response.(SCEPResponse) return resp.Data, resp.Err } func MakeServerEndpoints(svc Service) *Endpoints { e := MakeSCEPEndpoint(svc) return &Endpoints{ GetEndpoint: e, PostEndpoint: e, } } // MakeClientEndpoints returns an Endpoints struct where each endpoint invokes // the corresponding method on the remote instance, via a transport/http.Client. // Useful in a SCEP client. func MakeClientEndpoints(instance string) (*Endpoints, error) { if !strings.HasPrefix(instance, "http") { instance = "http://" + instance } tgt, err := url.Parse(instance) if err != nil { return nil, err } options := []httptransport.ClientOption{} return &Endpoints{ GetEndpoint: httptransport.NewClient( "GET", tgt, EncodeSCEPRequest, DecodeSCEPResponse, options...).Endpoint(), PostEndpoint: httptransport.NewClient( "POST", tgt, EncodeSCEPRequest, DecodeSCEPResponse, options...).Endpoint(), }, nil } func MakeSCEPEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(SCEPRequest) resp := SCEPResponse{operation: req.Operation} switch req.Operation { case "GetCACaps": resp.Data, resp.Err = svc.GetCACaps(ctx) case "GetCACert": resp.Data, resp.CACertNum, resp.Err = svc.GetCACert(ctx, string(req.Message)) case "PKIOperation": resp.Data, resp.Err = svc.PKIOperation(ctx, req.Message) default: return nil, errors.New("operation not implemented") } return resp, nil } } // SCEPRequest is a SCEP server request. type SCEPRequest struct { Operation string Message []byte } func (r SCEPRequest) scepOperation() string { return r.Operation } // SCEPResponse is a SCEP server response. // Business errors will be encoded as a CertRep message // with pkiStatus FAILURE and a failInfo attribute. type SCEPResponse struct { operation string CACertNum int Data []byte Err error } func (r SCEPResponse) scepOperation() string { return r.operation } // EndpointLoggingMiddleware returns an endpoint middleware that logs the // duration of each invocation, and the resulting error, if any. func EndpointLoggingMiddleware(logger log.Logger) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { var keyvals []interface{} // check if this is a scep endpoint, if it is, append the method to the log. if oper, ok := request.(interface { scepOperation() string }); ok { keyvals = append(keyvals, "op", oper.scepOperation()) } defer func(begin time.Time) { logger.Log(append(keyvals, "error", err, "took", time.Since(begin))...) }(time.Now()) return next(ctx, request) } } } golang-github-micromdm-scep-2.0.0/server/service.go000066400000000000000000000075721407235022200223020ustar00rootroot00000000000000package scepserver import ( "context" "crypto/rsa" "crypto/x509" "errors" "github.com/micromdm/scep/v2/scep" "github.com/go-kit/kit/log" ) // Service is the interface for all supported SCEP server operations. type Service interface { // GetCACaps returns a list of options // which are supported by the server. GetCACaps(ctx context.Context) ([]byte, error) // GetCACert returns CA certificate or // a CA certificate chain with intermediates // in a PKCS#7 Degenerate Certificates format // message is an optional string for the CA GetCACert(ctx context.Context, message string) ([]byte, int, error) // PKIOperation handles incoming SCEP messages such as PKCSReq and // sends back a CertRep PKIMessag. PKIOperation(ctx context.Context, msg []byte) ([]byte, error) // GetNextCACert returns a replacement certificate or certificate chain // when the old one expires. The response format is a PKCS#7 Degenerate // Certificates type. GetNextCACert(ctx context.Context) ([]byte, error) } type service struct { // The service certificate and key for SCEP exchanges. These are // quite likely the same as the CA keypair but may be its own SCEP // specific keypair in the case of e.g. RA (proxy) operation. crt *x509.Certificate key *rsa.PrivateKey // Optional additional CA certificates for e.g. RA (proxy) use. // Only used in this service when responding to GetCACert. addlCa []*x509.Certificate // The (chainable) CSR signing function. Intended to handle all // SCEP request functionality such as CSR & challenge checking, CA // issuance, RA proxying, etc. signer CSRSigner /// info logging is implemented in the service middleware layer. debugLogger log.Logger } func (svc *service) GetCACaps(ctx context.Context) ([]byte, error) { defaultCaps := []byte("Renewal\nSHA-1\nSHA-256\nAES\nDES3\nSCEPStandard\nPOSTPKIOperation") return defaultCaps, nil } func (svc *service) GetCACert(ctx context.Context, _ string) ([]byte, int, error) { if svc.crt == nil { return nil, 0, errors.New("missing CA certificate") } if len(svc.addlCa) < 1 { return svc.crt.Raw, 1, nil } certs := []*x509.Certificate{svc.crt} certs = append(certs, svc.addlCa...) data, err := scep.DegenerateCertificates(certs) return data, len(svc.addlCa) + 1, err } func (svc *service) PKIOperation(ctx context.Context, data []byte) ([]byte, error) { msg, err := scep.ParsePKIMessage(data, scep.WithLogger(svc.debugLogger)) if err != nil { return nil, err } if err := msg.DecryptPKIEnvelope(svc.crt, svc.key); err != nil { return nil, err } crt, err := svc.signer.SignCSR(msg.CSRReqMessage) if err == nil && crt == nil { err = errors.New("no signed certificate") } if err != nil { svc.debugLogger.Log("msg", "failed to sign CSR", "err", err) certRep, err := msg.Fail(svc.crt, svc.key, scep.BadRequest) return certRep.Raw, err } certRep, err := msg.Success(svc.crt, svc.key, crt) return certRep.Raw, err } func (svc *service) GetNextCACert(ctx context.Context) ([]byte, error) { panic("not implemented") } // ServiceOption is a server configuration option type ServiceOption func(*service) error // WithLogger configures a logger for the SCEP Service. // By default, a no-op logger is used. func WithLogger(logger log.Logger) ServiceOption { return func(s *service) error { s.debugLogger = logger return nil } } // WithAddlCA appends an additional certificate to the slice of CA certs func WithAddlCA(ca *x509.Certificate) ServiceOption { return func(s *service) error { s.addlCa = append(s.addlCa, ca) return nil } } // NewService creates a new scep service func NewService(crt *x509.Certificate, key *rsa.PrivateKey, signer CSRSigner, opts ...ServiceOption) (Service, error) { s := &service{ crt: crt, key: key, signer: signer, debugLogger: log.NewNopLogger(), } for _, opt := range opts { if err := opt(s); err != nil { return nil, err } } return s, nil } golang-github-micromdm-scep-2.0.0/server/service_bolt_test.go000066400000000000000000000125031407235022200243470ustar00rootroot00000000000000package scepserver_test import ( "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "fmt" "io/ioutil" "math/big" "os" "testing" "time" challengestore "github.com/micromdm/scep/v2/challenge/bolt" scepdepot "github.com/micromdm/scep/v2/depot" boltdepot "github.com/micromdm/scep/v2/depot/bolt" "github.com/micromdm/scep/v2/scep" scepserver "github.com/micromdm/scep/v2/server" "github.com/boltdb/bolt" ) func TestCaCert(t *testing.T) { // init bolt depot CA boltDepot := createDB(0666, nil) key, err := boltDepot.CreateOrLoadKey(2048) if err != nil { t.Fatal(err) } _, err = boltDepot.CreateOrLoadCA(key, 5, "MicroMDM", "US") if err != nil { t.Fatal(err) } // use exported interface depot := scepdepot.Depot(boltDepot) // load CA & key again certs, key, err := depot.CA([]byte{}) if err != nil { t.Fatal(err) } caCert := certs[0] // SCEP service svc, err := scepserver.NewService(caCert, key, scepdepot.NewSigner(depot)) if err != nil { t.Fatal(err) } // generate scep "client" keys, csr, cert selfKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } csrBytes, err := newCSR(selfKey, "ou", "loc", "province", "country", "cname", "org") if err != nil { t.Fatal(err) } csr, err := x509.ParseCertificateRequest(csrBytes) if err != nil { t.Fatal(err) } signerCert, err := selfSign(selfKey, csr) if err != nil { t.Fatal(err) } roots := x509.NewCertPool() roots.AddCert(caCert) var serCollector []*big.Int ctx := context.Background() for i := 0; i < 5; i++ { // check CA caBytes, num, err := svc.GetCACert(ctx, "") if err != nil { t.Fatal(err) } if have, want := num, 1; have != want { t.Errorf("i=%d, have %d, want %d", i, have, want) } if have, want := caBytes, caCert.Raw; !bytes.Equal(have, want) { t.Errorf("i=%d, have %v, want %v", i, have, want) } // create scep "client" request tmpl := &scep.PKIMessage{ MessageType: scep.PKCSReq, Recipients: []*x509.Certificate{caCert}, SignerKey: selfKey, SignerCert: signerCert, } msg, err := scep.NewCSRRequest(csr, tmpl) if err != nil { t.Fatal(err) } // submit to service respMsgBytes, err := svc.PKIOperation(ctx, msg.Raw) if err != nil { t.Fatal(err) } // read and decrypt reply respMsg, err := scep.ParsePKIMessage(respMsgBytes) if err != nil { t.Fatal(err) } err = respMsg.DecryptPKIEnvelope(signerCert, selfKey) if err != nil { t.Fatal(err) } // verify issued certificate is from the CA respCert := respMsg.CertRepMessage.Certificate opts := x509.VerifyOptions{ Roots: roots, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } chains, err := respCert.Verify(opts) if err != nil { t.Error(err) } if len(chains) < 1 { t.Error("no established chain between issued cert and CA") } // verify unique certificate serials for _, ser := range serCollector { if respCert.SerialNumber.Cmp(ser) == 0 { t.Error("seen serial number before!") } } serCollector = append(serCollector, respCert.SerialNumber) } } func createDB(mode os.FileMode, options *bolt.Options) *boltdepot.Depot { // Create temporary path. f, _ := ioutil.TempFile("", "bolt-") f.Close() os.Remove(f.Name()) db, err := bolt.Open(f.Name(), mode, options) if err != nil { panic(err.Error()) } d, err := boltdepot.NewBoltDepot(db) if err != nil { panic(err.Error()) } return d } func createChallengeStore(mode os.FileMode, options *bolt.Options) *challengestore.Depot { // Create temporary path. f, _ := ioutil.TempFile("", "bolt-challenge-") f.Close() os.Remove(f.Name()) db, err := bolt.Open(f.Name(), mode, options) if err != nil { panic(err.Error()) } d, err := challengestore.NewBoltDepot(db) if err != nil { panic(err.Error()) } return d } func newCSR(priv *rsa.PrivateKey, ou string, locality string, province string, country string, cname, org string) ([]byte, error) { subj := pkix.Name{ CommonName: cname, } if len(org) > 0 { subj.Organization = []string{org} } if len(ou) > 0 { subj.OrganizationalUnit = []string{ou} } if len(province) > 0 { subj.Province = []string{province} } if len(locality) > 0 { subj.Locality = []string{locality} } if len(country) > 0 { subj.Country = []string{country} } template := &x509.CertificateRequest{ Subject: subj, } return x509.CreateCertificateRequest(rand.Reader, template, priv) } func selfSign(priv *rsa.PrivateKey, csr *x509.CertificateRequest) (*x509.Certificate, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, fmt.Errorf("failed to generate serial number: %s", err) } notBefore := time.Now() notAfter := notBefore.Add(time.Hour * 1) template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: "SCEP SIGNER", Organization: csr.Subject.Organization, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return nil, err } return x509.ParseCertificate(derBytes) } golang-github-micromdm-scep-2.0.0/server/service_logging.go000066400000000000000000000023231407235022200237750ustar00rootroot00000000000000package scepserver import ( "context" "time" "github.com/go-kit/kit/log" ) type loggingService struct { logger log.Logger Service } // NewLoggingService creates adds logging to the SCEP service func NewLoggingService(logger log.Logger, s Service) Service { return &loggingService{logger, s} } func (mw *loggingService) GetCACaps(ctx context.Context) (caps []byte, err error) { defer func(begin time.Time) { _ = mw.logger.Log( "method", "GetCACaps", "err", err, "took", time.Since(begin), ) }(time.Now()) caps, err = mw.Service.GetCACaps(ctx) return } func (mw *loggingService) GetCACert(ctx context.Context, message string) (cert []byte, certNum int, err error) { defer func(begin time.Time) { _ = mw.logger.Log( "method", "GetCACert", "message", message, "err", err, "took", time.Since(begin), ) }(time.Now()) cert, certNum, err = mw.Service.GetCACert(ctx, message) return } func (mw *loggingService) PKIOperation(ctx context.Context, data []byte) (certRep []byte, err error) { defer func(begin time.Time) { _ = mw.logger.Log( "method", "PKIOperation", "err", err, "took", time.Since(begin), ) }(time.Now()) certRep, err = mw.Service.PKIOperation(ctx, data) return } golang-github-micromdm-scep-2.0.0/server/transport.go000066400000000000000000000105071407235022200226660ustar00rootroot00000000000000package scepserver import ( "bytes" "context" "encoding/base64" "fmt" "io" "io/ioutil" "net/http" "net/url" kitlog "github.com/go-kit/kit/log" kithttp "github.com/go-kit/kit/transport/http" "github.com/gorilla/mux" "github.com/groob/finalizer/logutil" "github.com/pkg/errors" ) func MakeHTTPHandler(e *Endpoints, svc Service, logger kitlog.Logger) http.Handler { opts := []kithttp.ServerOption{ kithttp.ServerErrorLogger(logger), kithttp.ServerFinalizer(logutil.NewHTTPLogger(logger).LoggingFinalizer), } r := mux.NewRouter() r.Methods("GET").Path("/scep").Handler(kithttp.NewServer( e.GetEndpoint, decodeSCEPRequest, encodeSCEPResponse, opts..., )) r.Methods("POST").Path("/scep").Handler(kithttp.NewServer( e.PostEndpoint, decodeSCEPRequest, encodeSCEPResponse, opts..., )) return r } // EncodeSCEPRequest encodes a SCEP HTTP Request. Used by the client. func EncodeSCEPRequest(ctx context.Context, r *http.Request, request interface{}) error { req := request.(SCEPRequest) params := r.URL.Query() params.Set("operation", req.Operation) switch r.Method { case "GET": if len(req.Message) > 0 { var msg string if req.Operation == "PKIOperation" { msg = base64.URLEncoding.EncodeToString(req.Message) } else { msg = string(req.Message) } params.Set("message", msg) } r.URL.RawQuery = params.Encode() return nil case "POST": body := bytes.NewReader(req.Message) // recreate the request here because IIS does not support chunked encoding by default // and Go doesn't appear to set Content-Length if we use an io.ReadCloser u := r.URL u.RawQuery = params.Encode() rr, err := http.NewRequest("POST", u.String(), body) rr.Header.Set("Content-Type", "application/octet-stream") if err != nil { return errors.Wrapf(err, "creating new POST request for %s", req.Operation) } *r = *rr return nil default: return fmt.Errorf("scep: %s method not supported", r.Method) } } const maxPayloadSize = 2 << 20 func decodeSCEPRequest(ctx context.Context, r *http.Request) (interface{}, error) { msg, err := message(r) if err != nil { return nil, err } defer r.Body.Close() request := SCEPRequest{ Message: msg, Operation: r.URL.Query().Get("operation"), } return request, nil } // extract message from request func message(r *http.Request) ([]byte, error) { switch r.Method { case "GET": var msg string q := r.URL.Query() if _, ok := q["message"]; ok { msg = q.Get("message") } op := q.Get("operation") if op == "PKIOperation" { msg2, err := url.PathUnescape(msg) if err != nil { return nil, err } return base64.StdEncoding.DecodeString(msg2) } return []byte(msg), nil case "POST": return ioutil.ReadAll(io.LimitReader(r.Body, maxPayloadSize)) default: return nil, errors.New("method not supported") } } // EncodeSCEPResponse writes a SCEP response back to the SCEP client. func encodeSCEPResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { resp := response.(SCEPResponse) if resp.Err != nil { http.Error(w, resp.Err.Error(), http.StatusInternalServerError) return nil } w.Header().Set("Content-Type", contentHeader(resp.operation, resp.CACertNum)) w.Write(resp.Data) return nil } // DecodeSCEPResponse decodes a SCEP response func DecodeSCEPResponse(ctx context.Context, r *http.Response) (interface{}, error) { if r.StatusCode != http.StatusOK && r.StatusCode >= 400 { body, _ := ioutil.ReadAll(io.LimitReader(r.Body, 4096)) return nil, fmt.Errorf("http request failed with status %s, msg: %s", r.Status, string(body), ) } data, err := ioutil.ReadAll(io.LimitReader(r.Body, maxPayloadSize)) if err != nil { return nil, err } defer r.Body.Close() resp := SCEPResponse{ Data: data, } header := r.Header.Get("Content-Type") if header == certChainHeader { // we only set it to two to indicate a cert chain. // the actual number of certs will be in the payload. resp.CACertNum = 2 } return resp, nil } const ( certChainHeader = "application/x-x509-ca-ra-cert" leafHeader = "application/x-x509-ca-cert" pkiOpHeader = "application/x-pki-message" ) func contentHeader(op string, certNum int) string { switch op { case "GetCACert": if certNum > 1 { return certChainHeader } return leafHeader case "PKIOperation": return pkiOpHeader default: return "text/plain" } } golang-github-micromdm-scep-2.0.0/server/transport_test.go000066400000000000000000000103441407235022200237240ustar00rootroot00000000000000package scepserver_test import ( "bytes" "context" "crypto/x509" "encoding/base64" "io/ioutil" "net/http" "net/http/httptest" "os" "strings" "testing" "github.com/micromdm/scep/v2/depot" filedepot "github.com/micromdm/scep/v2/depot/file" scepserver "github.com/micromdm/scep/v2/server" kitlog "github.com/go-kit/kit/log" ) func TestCACaps(t *testing.T) { server, _, teardown := newServer(t) defer teardown() url := server.URL + "/scep?operation=GetCACaps" resp, err := http.Get(url) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Error("expected", http.StatusOK, "got", resp.StatusCode) } } func TestEncodePKCSReq_Request(t *testing.T) { pkcsreq := loadTestFile(t, "../scep/testdata/PKCSReq.der") msg := scepserver.SCEPRequest{ Operation: "PKIOperation", Message: pkcsreq, } methods := []string{"POST", "GET"} for _, method := range methods { t.Run(method, func(t *testing.T) { r := httptest.NewRequest(method, "http://acme.co/scep", nil) rr := *r if err := scepserver.EncodeSCEPRequest(context.Background(), &rr, msg); err != nil { t.Fatal(err) } q := r.URL.Query() if have, want := q.Get("operation"), msg.Operation; have != want { t.Errorf("have %s, want %s", have, want) } if method == "POST" { if have, want := rr.ContentLength, int64(len(msg.Message)); have != want { t.Errorf("have %d, want %d", have, want) } } if method == "GET" { if q.Get("message") == "" { t.Errorf("expected GET PKIOperation to have a non-empty message field") } } }) } } func TestGetCACertMessage(t *testing.T) { testMsg := "testMsg" sr := scepserver.SCEPRequest{Operation: "GetCACert", Message: []byte(testMsg)} req, err := http.NewRequest("GET", "http://127.0.0.1:8080/scep", nil) if err != nil { t.Fatal(err) } err = scepserver.EncodeSCEPRequest(context.Background(), req, sr) if err != nil { t.Fatal(err) } if !strings.Contains(req.URL.RawQuery, "message="+testMsg) { t.Fatal("RawQuery does not contain message") } } func TestPKIOperation(t *testing.T) { server, _, teardown := newServer(t) defer teardown() pkcsreq := loadTestFile(t, "../scep/testdata/PKCSReq.der") body := bytes.NewReader(pkcsreq) url := server.URL + "/scep?operation=PKIOperation" resp, err := http.Post(url, "", body) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Error("expected", http.StatusOK, "got", resp.StatusCode) } } func TestPKIOperationGET(t *testing.T) { server, _, teardown := newServer(t) defer teardown() pkcsreq := loadTestFile(t, "../scep/testdata/PKCSReq.der") message := base64.StdEncoding.EncodeToString(pkcsreq) req, err := http.NewRequest("GET", server.URL+"/scep", nil) if err != nil { t.Fatal(err) } params := req.URL.Query() params.Set("operation", "PKIOperation") params.Set("message", message) req.URL.RawQuery = params.Encode() resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Error("expected", http.StatusOK, "got", resp.StatusCode) } } func newServer(t *testing.T, opts ...scepserver.ServiceOption) (*httptest.Server, scepserver.Service, func()) { var err error var depot depot.Depot // cert storage { depot, err = filedepot.NewFileDepot("../scep/testdata/testca") if err != nil { t.Fatal(err) } depot = &noopDepot{depot} } crt, key, err := depot.CA([]byte{}) var svc scepserver.Service // scep service { svc, err = scepserver.NewService(crt[0], key, scepserver.NopCSRSigner()) if err != nil { t.Fatal(err) } } logger := kitlog.NewNopLogger() e := scepserver.MakeServerEndpoints(svc) handler := scepserver.MakeHTTPHandler(e, svc, logger) server := httptest.NewServer(handler) teardown := func() { server.Close() os.Remove("../scep/testdata/testca/serial") os.Remove("../scep/testdata/testca/index.txt") } return server, svc, teardown } type noopDepot struct{ depot.Depot } func (d *noopDepot) Put(name string, crt *x509.Certificate) error { return nil } /* helpers */ const ( rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" certificatePEMBlockType = "CERTIFICATE" ) func loadTestFile(t *testing.T, path string) []byte { data, err := ioutil.ReadFile(path) if err != nil { t.Fatal(err) } return data }