Compare commits

...

10 Commits

Author SHA1 Message Date
T. R. Bernstein
853e2a909f refactor: Split large main file 2026-04-30 02:09:16 +02:00
T. R. Bernstein
4e4dd2eaae feat: implement multi CA serving 2026-04-30 02:08:56 +02:00
T. R. Bernstein
00ac7628d4 doc: adapt readme wording and include hint about Spacebar 2026-04-30 01:31:53 +02:00
Renovate Bot
520f329223 chore(deps): update goreleaser/goreleaser docker digest to 579eee2 2026-04-21 22:31:09 +00:00
Renovate Bot
7814486f9e chore(deps): update alpine docker digest to 5b10f43 2026-04-16 10:35:40 +00:00
Renovate Bot
2228db8de8 chore(deps): update goreleaser/goreleaser docker digest to 69d129e 2026-04-15 22:34:36 +00:00
Florian Bauer
b4470d0137 fix(deps): update module golang.org/x/crypto to v0.50.0
See merge request https://ref.ci/fsrvcorp/pki/ocspcrl/-/merge_requests/20
2026-04-15 16:36:18 +00:00
Renovate Bot
8bbd505588 squash: fix(deps): update module golang.org/x/crypto to v0.50.0
Squashed commit of the following:

* fix(deps): update module golang.org/x/crypto to v0.50.0

See merge request https://ref.ci/fsrvcorp/pki/ocspcrl/-/merge_requests/20
2026-04-15 16:36:17 +00:00
Renovate Bot
be48c53fad chore(deps): update goreleaser/goreleaser docker digest to 5be644c 2026-03-31 22:32:26 +00:00
Renovate Bot
b4e70ecf63 chore(deps): update goreleaser/goreleaser docker digest to c3c61eb 2026-03-30 22:31:42 +00:00
12 changed files with 419 additions and 189 deletions

View File

@@ -13,7 +13,7 @@ include:
packages:
stage: release
image: goreleaser/goreleaser@sha256:848430a900a83ca0e18f2f149fb4ddcdaea74a667aa07224268b97d448833591
image: goreleaser/goreleaser@sha256:579eee23514fa647adcc669b5875f866f1c1faf5a0464aec4614a9121684c06c
script:
- git reset --hard $CI_COMMIT_SHA
- git clean -ffdx
@@ -30,7 +30,7 @@ packages:
deb mirror:
stage: release
image: alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
image: alpine@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11
only:
- tags
dependencies:

View File

@@ -1,16 +1,61 @@
# OCSPCRL
# OCSP Server
OCSPCRL is a minimal implementation of both a OCSP and CRL server in Golang. It provides the following http endpoints:
OCSP Server is a minimal implementation of both an OCSP and CRL server in Golang, using a CRL as the source for both interfaces.
Originally created by Florian Bauer and now adapted for Spacebar. A single instance can serve any number of CAs (e.g. a root CA and several intermediates).
| Endpoint | Description |
|------------|----------------------------------------------------------|
| `/ocsp` | OCSP responder supporting both `GET` and `POST` requests |
| `/crl` | CRL responder in DER format |
| `/crl.pem` | CRL responder in PEM format |
| `/ca` | Issuer CA certificate in DER format |
| `/ca.pem` | Issuer CA certificate in PEM format |
## Configuration
All what you need is to provide a CRL file, the root certificate and cert/key with extendedKeyUsage `OCSPSigning` to allow the OCSP server to sign the OCSP responses.
When using OCSP, the certificate is checked against the CRL for validity.
Point the server at a single directory that contains one subdirectory per CA. The subdirectory name is used as the route prefix.
Synchronization of the CAs CRL is out of scope of this project. You can use any mechanism to update the CRL file. Just notify the ocspcrl server process via `SIGHUP` signal to reload the CRL file.
```
cas/
├── root/
│ ├── ca.crt
│ ├── responder.crt
│ ├── key.pem
│ └── crl.pem
├── hr/
│ ├── ca.crt
│ ├── responder.crt
│ ├── key.pem
│ └── crl.pem
└── it/
├── ca.crt
├── responder.crt
├── key.pem
└── crl.pem
```
Each CA subdirectory must contain:
| File | Description |
|-----------------|--------------------------------------------------------------------------|
| `ca.crt` | The CA certificate (PEM) |
| `responder.crt` | The OCSP responder certificate with `extendedKeyUsage = OCSPSigning` |
| `key.pem` | The private key for the responder certificate (PEM) |
| `crl.pem` | The current CRL issued by the CA (PEM or DER) |
Run the server with:
```
ocspcrl --cas-directory /path/to/cas
```
## Endpoints
For every CA subdirectory `<route-prefix>`, the following endpoints are exposed:
| Endpoint | Content-Type | Description |
|-----------------------------|-----------------------------|----------------------------------------------------------|
| `<route-prefix>/ocsp` | (OCSP) | OCSP responder supporting both `GET` and `POST` requests |
| `<route-prefix>/ocsp/` | (OCSP) | OCSP responder supporting both `GET` and `POST` requests |
| `<route-prefix>/crl` | `application/pkix-cert` | CRL in DER form |
| `<route-prefix>/crl.pem` | `application/pkix-crl` | CRL in PEM form |
| `<route-prefix>/ca` | `application/pkix-cert` | CA certificate in DER form |
| `<route-prefix>/ca.pem` | `application/x-x509-ca-cert`| CA certificate in PEM form |
For example, with the layout above, the OCSP endpoint for the `hr` intermediate is `https://<host>/hr/ocsp` and its CRL is at `https://<host>/hr/crl.pem`.
## Reloading the CRLs
Synchronization of the CAs' CRLs is out of scope of this project. You can use any mechanism to update the CRL files. Notify the `ocspcrl` server process via `SIGHUP` to reload every CA's CRL from disk.

38
ca.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"crypto/x509"
"sync"
"ocspcrl/internal/metrics"
"ocspcrl/internal/ocsp_source"
)
type caInstance struct {
name string
crlPath string
caCertificate *x509.Certificate
source *ocsp_source.CrlSource
crlMutex sync.RWMutex
crl *x509.RevocationList
}
func (c *caInstance) reloadCrl() error {
crl, loadError := loadCrlFromFile(c.crlPath)
if loadError != nil {
return loadError
}
c.crlMutex.Lock()
c.crl = crl
c.crlMutex.Unlock()
metrics.CrlEntries.WithLabelValues(c.name).Set(float64(len(crl.RevokedCertificateEntries)))
c.source.UseCrl(*crl)
return nil
}
func (c *caInstance) currentCrl() *x509.RevocationList {
c.crlMutex.RLock()
defer c.crlMutex.RUnlock()
return c.crl
}

130
ca_loader.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"ocspcrl/internal/ocsp_source"
)
const (
crlFileName = "crl.pem"
keyFileName = "key.pem"
responderFileName = "responder.crt"
caFileName = "ca.crt"
)
type caFiles struct {
key string
responder string
ca string
crl string
}
func newCaFiles(directory string) caFiles {
return caFiles{
key: filepath.Join(directory, keyFileName),
responder: filepath.Join(directory, responderFileName),
ca: filepath.Join(directory, caFileName),
crl: filepath.Join(directory, crlFileName),
}
}
func (f caFiles) ensureExist(caName string) error {
for _, path := range []string{f.key, f.responder, f.ca, f.crl} {
if _, statError := os.Stat(path); statError != nil {
return fmt.Errorf("ca %q: %w", caName, statError)
}
}
return nil
}
func verifyResponderIssuedByCa(responder tls.Certificate, ca *x509.Certificate) error {
if responder.Leaf == nil {
return fmt.Errorf("responder leaf certificate could not be parsed")
}
if !bytes.Equal(ca.RawSubject, responder.Leaf.RawIssuer) {
return fmt.Errorf("responder certificate issuer does not match ca certificate subject; %+q != %+q",
ca.Subject.String(), responder.Leaf.Issuer.String())
}
return nil
}
func loadCa(name, directory string) (*caInstance, error) {
files := newCaFiles(directory)
if existsError := files.ensureExist(name); existsError != nil {
return nil, existsError
}
responderKeyPair, loadResponderError := tls.LoadX509KeyPair(files.responder, files.key)
if loadResponderError != nil {
return nil, fmt.Errorf("ca %q: failed to load responder key pair: %w", name, loadResponderError)
}
caCertificate, loadCaError := loadCertificateFromFile(files.ca)
if loadCaError != nil {
return nil, fmt.Errorf("ca %q: failed to load ca certificate: %w", name, loadCaError)
}
if verifyError := verifyResponderIssuedByCa(responderKeyPair, caCertificate); verifyError != nil {
return nil, fmt.Errorf("ca %q: %w", name, verifyError)
}
instance := &caInstance{
name: name,
crlPath: files.crl,
caCertificate: caCertificate,
source: ocsp_source.NewCrlSource(caCertificate, responderKeyPair),
}
if reloadError := instance.reloadCrl(); reloadError != nil {
return nil, fmt.Errorf("ca %q: failed to load crl: %w", name, reloadError)
}
return instance, nil
}
func listCaSubdirectories(rootDir string) ([]string, error) {
entries, readDirError := os.ReadDir(rootDir)
if readDirError != nil {
return nil, fmt.Errorf("failed to read cas directory: %w", readDirError)
}
names := []string{}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
if strings.HasPrefix(entry.Name(), ".") {
continue
}
names = append(names, entry.Name())
}
sort.Strings(names)
return names, nil
}
func discoverCas(rootDir string) ([]*caInstance, error) {
names, listError := listCaSubdirectories(rootDir)
if listError != nil {
return nil, listError
}
if len(names) == 0 {
return nil, fmt.Errorf("no ca subdirectories found in %s", rootDir)
}
cas := make([]*caInstance, 0, len(names))
for _, name := range names {
instance, loadError := loadCa(name, filepath.Join(rootDir, name))
if loadError != nil {
return nil, loadError
}
cas = append(cas, instance)
}
return cas, nil
}

25
config.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"github.com/alecthomas/kingpin/v2"
)
type configuration struct {
casDirectory string
applicationListenAddress string
metricsListenAddress string
}
func parseConfiguration(args []string) *configuration {
config := &configuration{}
app := kingpin.New("ocspcrl", "OCSP responder / CRL server (multi-CA)")
app.HelpFlag.Short('h')
app.Flag("cas-directory", "Path to a directory containing one subdirectory per CA. Each subdirectory must contain ca.crt, responder.crt, key.pem and crl.pem. The subdirectory name is used as the route prefix.").
Envar("CAS_DIRECTORY").Required().ExistingDirVar(&config.casDirectory)
app.Flag("web.listen-address", "Address for application endpoint").
Envar("WEB_LISTEN_ADDRESS").Default(":8080").StringVar(&config.applicationListenAddress)
app.Flag("metrics.listen-address", "Address for metrics endpoint").
Envar("METRICS_LISTEN_ADDRESS").Default("[::1]:8081").StringVar(&config.metricsListenAddress)
kingpin.MustParse(app.Parse(args))
return config
}

4
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/cloudflare/cfssl v1.6.5
github.com/prometheus/client_golang v1.23.2
golang.org/x/crypto v0.49.0
golang.org/x/crypto v0.50.0
)
require (
@@ -24,6 +24,6 @@ require (
github.com/prometheus/procfs v0.16.1 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.43.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
)

8
go.sum
View File

@@ -63,10 +63,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -26,11 +26,11 @@ var (
Buckets: prometheus.ExponentialBuckets(0.0001, 2, 10),
}, []string{labelPath})
CrlEntries = prometheus.NewGauge(prometheus.GaugeOpts{
CrlEntries = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "ocspcrl",
Name: "crl_entries_total",
Help: "Number of entries in the CRL",
})
}, []string{"ca"})
)
func init() {

190
main.go
View File

@@ -1,203 +1,59 @@
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/alecthomas/kingpin/v2"
cfocsp "github.com/cloudflare/cfssl/ocsp"
"github.com/prometheus/client_golang/prometheus/promhttp"
"ocspcrl/internal/metrics"
"ocspcrl/internal/ocsp_source"
)
type loadCrlFunction func() error
func loadCrlFromFile(path string) (*x509.RevocationList, error) {
crlContent, openCrlError := os.ReadFile(path)
if openCrlError != nil {
return nil, openCrlError
}
// if the file contains a pem block, decode it
// otherwise, assume it is a DER encoded CRL
crlBlock := &pem.Block{}
if bytes.Contains(crlContent, []byte("BEGIN")) {
block, rest := pem.Decode(crlContent)
if len(rest) > 0 {
return nil, fmt.Errorf("failed to decode crl")
func startServer(server *http.Server, label string) <-chan struct{} {
closed := make(chan struct{})
go func() {
log.Printf("starting %s server on %+q", label, server.Addr)
if listenError := server.ListenAndServe(); !errors.Is(listenError, http.ErrServerClosed) {
log.Printf("%s server error: %v", label, listenError)
}
crlBlock = block
} else {
crlBlock = &pem.Block{Type: "X509 CRL", Bytes: crlContent}
}
crl, parseCrlError := x509.ParseRevocationList(crlBlock.Bytes)
if parseCrlError != nil {
return nil, parseCrlError
}
return crl, nil
}
func reloadCrlWorker(signal chan os.Signal, loadCrlFunc loadCrlFunction) {
defer log.Println("reload crl worker stopped")
for {
select {
case _, ok := <-signal:
if !ok {
return
}
loadCrlError := loadCrlFunc()
if loadCrlError != nil {
log.Printf("failed to reload crl: %v", loadCrlError)
} else {
log.Println("reloaded crl")
}
}
}
}
type responder struct {
certificatePath string
keyPath string
}
type crlSourceFile struct {
path string
}
type configuration struct {
responder *responder
caCrtPath string
crlSourceType string
crlSourceFile *crlSourceFile
applicationListenAddress string
metricsListenAddress string
close(closed)
}()
return closed
}
func main() {
log.SetFlags(log.Lshortfile)
config := &configuration{
responder: &responder{},
crlSourceFile: &crlSourceFile{},
}
app := kingpin.New("ocspcrl", "OCSP responder / CRL server")
app.HelpFlag.Short('h')
app.Flag("responder.certificate-path", "Path to the responder certificate").Envar("RESPONDER_CERTIFICATE_PATH").Required().ExistingFileVar(&config.responder.certificatePath)
app.Flag("responder.key-path", "Path to the responder key").Envar("RESPONDER_KEY_PATH").Required().ExistingFileVar(&config.responder.keyPath)
app.Flag("ca-crt-path", "Path to the CA certificate").Envar("CA_CRT_PATH").Required().ExistingFileVar(&config.caCrtPath)
app.Flag("crl-source-type", "Type of CRL source").Envar("CRL_SOURCE").Default("file").EnumVar(&config.crlSourceType, "file")
app.Flag("source.file.path", "Path to the CRL file").Envar("SOURCE_FILE_PATH").ExistingFileVar(&config.crlSourceFile.path)
app.Flag("web.listen-address", "Address for application endpoint").Envar("WEB_LISTEN_ADDRESS").Default(":8080").StringVar(&config.applicationListenAddress)
app.Flag("metrics.listen-address", "Address for metrics endpoint").Envar("METRICS_LISTEN_ADDRESS").Default("[::1]:8081").StringVar(&config.metricsListenAddress)
kingpin.MustParse(app.Parse(os.Args[1:]))
config := parseConfiguration(os.Args[1:])
responderKeyPair, loadResponderKeyPairError := tls.LoadX509KeyPair(config.responder.certificatePath, config.responder.keyPath)
if loadResponderKeyPairError != nil {
log.Fatalf("failed to load responder key pair: %v", loadResponderKeyPairError)
cas, discoverError := discoverCas(config.casDirectory)
if discoverError != nil {
log.Fatalf("failed to load cas: %v", discoverError)
}
caCrtContent, openCaCrtError := os.ReadFile(config.caCrtPath)
if openCaCrtError != nil {
log.Fatalf("failed to open ca certificate: %v", openCaCrtError)
}
block, rest := pem.Decode(caCrtContent)
if len(rest) > 0 {
log.Fatalln("failed to decode ca certificate")
}
caCertificate, loadCaCertificateError := x509.ParseCertificate(block.Bytes)
if loadCaCertificateError != nil {
log.Fatalf("failed to parse ca certificate: %v", loadCaCertificateError)
}
applicationRouter := buildApplicationRouter(cas)
if !bytes.Equal(caCertificate.RawSubject, responderKeyPair.Leaf.RawIssuer) {
log.Fatalf("responder certificate issuer does not match ca certificate subject; %+q != %+q", caCertificate.Subject.String(), responderKeyPair.Leaf.Issuer.String())
}
terminationChan := make(chan os.Signal, 1)
signal.Notify(terminationChan, syscall.SIGINT, syscall.SIGTERM)
source := ocsp_source.NewCrlSource(caCertificate, responderKeyPair)
crl := &x509.RevocationList{}
loadCrl := func() error {
curlCandidate, loadCrlError := loadCrlFromFile(config.crlSourceFile.path)
if loadCrlError != nil {
return loadCrlError
}
metrics.CrlEntries.Set(float64(len(curlCandidate.RevokedCertificateEntries)))
source.UseCrl(*curlCandidate)
crl = curlCandidate
return nil
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// initial load of the CRL
if loadCrlError := loadCrl(); loadCrlError != nil {
log.Fatalf("failed to load crl: %v", loadCrlError)
}
// on HUP reload the CRL
hupChan := make(chan os.Signal, 1)
signal.Notify(hupChan, syscall.SIGHUP)
go reloadCrlWorker(hupChan, loadCrl)
responder := cfocsp.NewResponder(source, nil)
applicationRouter := http.NewServeMux()
applicationRouter.Handle("/ocsp", responder)
applicationRouter.Handle("/ocsp/", http.StripPrefix("/ocsp/", responder))
applicationRouter.HandleFunc("/crl", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/pkix-crl")
w.Write(crl.Raw)
})
applicationRouter.HandleFunc("/crl.pem", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/pkix-crl")
pem.Encode(w, &pem.Block{Type: "X509 CRL", Bytes: crl.Raw})
})
applicationRouter.HandleFunc("/ca", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/pkix-cert")
w.Write(caCertificate.Raw)
})
applicationRouter.HandleFunc("/ca.pem", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: caCertificate.Raw})
})
go runReloadWorker(hupChan, cas)
applicationServer := &http.Server{Addr: config.applicationListenAddress, Handler: metrics.Middleware(applicationRouter)}
metricsServer := &http.Server{Addr: config.metricsListenAddress, Handler: promhttp.Handler()}
applicationServerClosed := make(chan any)
metricsServerClosed := make(chan any)
go func() {
log.Printf("starting application server on %+q", config.applicationListenAddress)
if listenError := applicationServer.ListenAndServe(); !errors.Is(listenError, http.ErrServerClosed) {
log.Printf("application error: %v", listenError)
}
close(applicationServerClosed)
}()
go func() {
log.Printf("starting metrics server on %+q", config.metricsListenAddress)
if listenError := metricsServer.ListenAndServe(); !errors.Is(listenError, http.ErrServerClosed) {
log.Printf("metrics server error: %v", listenError)
}
close(metricsServerClosed)
}()
applicationServerClosed := startServer(applicationServer, "application")
metricsServerClosed := startServer(metricsServer, "metrics")
<-signalChan
<-terminationChan
close(hupChan)
applicationServer.Shutdown(nil)
metricsServer.Shutdown(nil)
applicationServer.Shutdown(context.TODO())
metricsServer.Shutdown(context.TODO())
<-applicationServerClosed
<-metricsServerClosed
}

50
pem_io.go Normal file
View File

@@ -0,0 +1,50 @@
package main
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
)
func decodeCrlBytes(content []byte) ([]byte, error) {
if !bytes.Contains(content, []byte("BEGIN")) {
return content, nil
}
block, rest := pem.Decode(content)
if block == nil {
return nil, fmt.Errorf("crl pem block could not be decoded")
}
if len(bytes.TrimSpace(rest)) > 0 {
return nil, fmt.Errorf("crl file contains trailing data")
}
return block.Bytes, nil
}
func loadCrlFromFile(path string) (*x509.RevocationList, error) {
content, readError := os.ReadFile(path)
if readError != nil {
return nil, readError
}
derBytes, decodeError := decodeCrlBytes(content)
if decodeError != nil {
return nil, fmt.Errorf("%s: %w", path, decodeError)
}
return x509.ParseRevocationList(derBytes)
}
func loadCertificateFromFile(path string) (*x509.Certificate, error) {
content, readError := os.ReadFile(path)
if readError != nil {
return nil, readError
}
block, rest := pem.Decode(content)
if block == nil {
return nil, fmt.Errorf("%s: certificate pem block could not be decoded", path)
}
if len(bytes.TrimSpace(rest)) > 0 {
return nil, fmt.Errorf("%s: certificate file contains trailing data", path)
}
return x509.ParseCertificate(block.Bytes)
}

27
reload.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"log"
"os"
)
func reloadAllCrls(cas []*caInstance) {
for _, ca := range cas {
if reloadError := ca.reloadCrl(); reloadError != nil {
log.Printf("failed to reload crl for ca %q: %v", ca.name, reloadError)
} else {
log.Printf("reloaded crl for ca %q", ca.name)
}
}
}
func runReloadWorker(signalChan <-chan os.Signal, cas []*caInstance) {
defer log.Println("reload crl worker stopped")
for {
_, ok := <-signalChan
if !ok {
return
}
reloadAllCrls(cas)
}
}

59
routes.go Normal file
View File

@@ -0,0 +1,59 @@
package main
import (
"encoding/pem"
"log"
"net/http"
cfocsp "github.com/cloudflare/cfssl/ocsp"
)
func writeBinary(w http.ResponseWriter, contentType string, body []byte) {
w.Header().Set("Content-Type", contentType)
w.Write(body)
}
func writePem(w http.ResponseWriter, contentType, blockType string, body []byte) {
w.Header().Set("Content-Type", contentType)
pem.Encode(w, &pem.Block{Type: blockType, Bytes: body})
}
func registerOcspRoutes(router *http.ServeMux, prefix string, ca *caInstance) {
responder := cfocsp.NewResponder(ca.source, nil)
router.Handle(prefix+"/ocsp", responder)
router.Handle(prefix+"/ocsp/", http.StripPrefix(prefix+"/ocsp/", responder))
}
func registerCrlRoutes(router *http.ServeMux, prefix string, ca *caInstance) {
router.HandleFunc(prefix+"/crl", func(w http.ResponseWriter, r *http.Request) {
writeBinary(w, "application/pkix-cert", ca.currentCrl().Raw)
})
router.HandleFunc(prefix+"/crl.pem", func(w http.ResponseWriter, r *http.Request) {
writePem(w, "application/pkix-crl", "X509 CRL", ca.currentCrl().Raw)
})
}
func registerCaCertificateRoutes(router *http.ServeMux, prefix string, ca *caInstance) {
router.HandleFunc(prefix+"/ca", func(w http.ResponseWriter, r *http.Request) {
writeBinary(w, "application/pkix-cert", ca.caCertificate.Raw)
})
router.HandleFunc(prefix+"/ca.pem", func(w http.ResponseWriter, r *http.Request) {
writePem(w, "application/x-x509-ca-cert", "CERTIFICATE", ca.caCertificate.Raw)
})
}
func registerCaRoutes(router *http.ServeMux, ca *caInstance) {
prefix := "/" + ca.name
registerOcspRoutes(router, prefix, ca)
registerCrlRoutes(router, prefix, ca)
registerCaCertificateRoutes(router, prefix, ca)
}
func buildApplicationRouter(cas []*caInstance) *http.ServeMux {
router := http.NewServeMux()
for _, ca := range cas {
registerCaRoutes(router, ca)
log.Printf("registered ca %q with routes under /%s/", ca.name, ca.name)
}
return router
}