Skip to content
This repository was archived by the owner on Mar 18, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ $ echo -n '{"Hostname": "test@yourremote.com"}' | openssl dgst -sha256 -hmac "yo
10ded103a220f14f02f9ee106a32348b1d0105cc8c40aa1c99ef1f115542a2ff
```

For `/list` and `/revoke` an optional `--admin-key` can be sent which should differ from `--key`. For those methods
you should instead use that key. By default `--admin-key` defaults to `--key`.

## OCSP

In addition to generating certs, locksmith can also be a OCSP responder. In your cfssl config, add
```
"ocsp_url": "https://locksmithIP:port/ocsp-verify",
```
to each profile you want to use ocsp verification. You'll also need to have a responses file and pass it to locksmith
via `--ocsp-responses-file`. The responses file should be base64-encoded responses separated by a space (or newline)
character. By default, generated certificates are NOT automatically added to the ocsp responses file. To add them
automatically, pass `--auto-ocsp-sign` to locksmith. The `/ocsp-verify` endpoint does not accept an HMAC signature.

In order to sign OCSP responses in cfssl you will need to generate a OCSP certificate/key and pass it to cfssl as
`-responder` and `-responder-key`.

Todo: add instructions for how to incorporate this into OpenSSL server

## Endpoints

### /generate
Expand All @@ -50,7 +69,7 @@ Generates a new certificate

#### Params
* `hostname` [string] the hostname to set as the CN for the certificate
* `rimestamp` [int] the current unix timestamp in seconds
* `timestamp` [int] the current unix timestamp in seconds
* `profile` [string|optional] the profile to send to cfssl
* `request` [hash|optional] the csr request to send to cfssl (optional if `--default-name-file` was specified)
* `CN` [string|optional] common name for the certificate (defaults to `hostname`)
Expand All @@ -60,4 +79,33 @@ Generates a new certificate

#### Result

Returns a ovpn file to save as `client.conf` (on Linux) and use with OpenVPN client
Returns the contents of a new ovpn file to save as `client.conf` (on Linux) and use with OpenVPN client.
*This does not return json*


### /list

Returns a list of previously generated certificates (assuming you passed `--certs-file`)

#### Params
* `timestamp` [int] the current unix timestamp in seconds

#### Result

Returns an array of certificates. Each certificate looks like:
```
{"hostname":"laptop","remoteAddr":"10.0.0.1","generated_at":1440113442,"certificate":"MIIEWD..."}


### /revoke

Generates a new certificate

#### Params
* `certificate` [string] base64 of certificate der, or pem-encoded certificate
* `reason` [int] the reason code for the revocation
* `timestamp` [int] the current unix timestamp in seconds

#### Result

Returns `{"success": true}` or non-200 status code.
152 changes: 152 additions & 0 deletions certs/certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Package certs handles persisting and loading certs
package certs

import (
"bufio"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"github.com/levenlabs/locksmith/config"
"log"
"os"
"time"
)

type saveReq struct {
Hostname string
RemoteAddr string
Certificate string
ReplyCh chan error
}

type Certificate struct {
Hostname string `json:"hostname"`
RemoteAddr string `json:"remoteAddr"`
Generated int64 `json:"generated_at"`
Certificate string `json:"certificate"`
}

type listReq struct {
ReplyCh chan []Certificate
}

var (
listCh = make(chan listReq)
saveCh = make(chan saveReq)
timeFmt = "2006-01-02T15:04:05"
fileFmt = "%s | %s | %s | %s"
)

func init() {
if config.CertsFile != "" {
// make sure we can write to the file
f, err := os.OpenFile(config.CertsFile, os.O_CREATE, 0600)
if err != nil {
log.Fatalf("Failed to create certs file: %s", err)
}
f.Close()
}

go func() {
var l listReq
var s saveReq
for {
select {
case l = <-listCh:
l.ReplyCh <- listCerts()
case s = <-saveCh:
s.ReplyCh <- saveCert(s.Hostname, s.RemoteAddr, s.Certificate)
}
}
}()
}

func SaveCert(hostname, remoteAddr, cert string) error {
r := make(chan error)
saveCh <- saveReq{
Hostname: hostname,
RemoteAddr: remoteAddr,
Certificate: cert,
ReplyCh: r,
}
return <-r
}

func saveCert(hostname, remoteAddr, cert string) error {
if config.CertsFile == "" {
return nil
}

b, _ := pem.Decode([]byte(cert))
if b == nil || len(b.Bytes) == 0 {
return errors.New("Error reading PEM certificate")
}

b64cert := base64.StdEncoding.EncodeToString(b.Bytes)
tf := time.Now().Format(timeFmt)
line := fmt.Sprintf(fileFmt, tf, hostname, remoteAddr, b64cert)

f, err := os.OpenFile(config.CertsFile, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return err
}

defer f.Close()
line += "\n"
if _, err = f.WriteString(line); err != nil {
return err
}
return nil
}

func ListCerts() []Certificate {
r := make(chan []Certificate)
listCh <- listReq{
ReplyCh: r,
}
return <-r
}

func listCerts() []Certificate {
// give this some starting capacity
c := make([]Certificate, 0, 16)
if config.CertsFile == "" {
return c
}
f, err := os.OpenFile(config.CertsFile, os.O_RDONLY, 0600)
if err != nil {
log.Printf("Error reading certs file: %s", err)
return c
}
defer f.Close()

s := bufio.NewScanner(f)
var l string
var cert Certificate
var t string
var gen time.Time
for s.Scan() {
l = s.Text()
if l == "" {
continue
}
cert = Certificate{}
_, err = fmt.Sscanf(l, fileFmt, &t, &cert.Hostname, &cert.RemoteAddr, &cert.Certificate)
if err != nil {
log.Printf("Error reading line from certs file: %s", err)
continue
}
gen, err = time.Parse(timeFmt, t)
if err != nil {
log.Printf("Error parsing time in certs file: %s", err)
continue
}
cert.Generated = gen.Unix()
c = append(c, cert)
}
if err = s.Err(); err != nil {
log.Printf("Error scanning certs file: %s", err)
}
return c
}
40 changes: 29 additions & 11 deletions cfssl/cfssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
"github.com/levenlabs/locksmith/config"
"io/ioutil"
"log"
"math"
"net/http"
"regexp"
"time"
)

var defaultName *csrName
Expand Down Expand Up @@ -63,6 +61,13 @@ type GenerateRequest struct {
Timestamp int64 `json:"timestamp,omitempty"`
}

type OCSPSignRequest struct {
Certificate string `json:"certificate"`
Status string `json:"status,omitempty"`
Reason int `json:"reason,omitempty"`
RevokedAt string `json:"revoked_at,omitempty"`
}

func init() {
if config.CFSSLAddr == "" {
log.Fatal("--cfssl-addr must be sent")
Expand All @@ -87,14 +92,6 @@ func GenerateCert(req *GenerateRequest, remoteAddr string) (cert string, key str
err = errors.New("Invalid hostname sent")
return
}
if config.TimestampDrift > 0 {
now := time.Now().UTC().Unix()
diff := math.Abs(float64(now - req.Timestamp))
if diff > config.TimestampDrift {
err = errors.New("Timestamp sent is outside of drift range")
return
}
}
// we can't check if Key == nil because Key isn't a pointer but we can check for size not existing
if req.Request.Key.Size == 0 {
req.Request.Key = *defaultKey
Expand All @@ -117,7 +114,6 @@ func GenerateCert(req *GenerateRequest, remoteAddr string) (cert string, key str
r.Request = req.Request
r.Profile = req.Profile
r.RemoteAddress = remoteAddr
r.Token = config.CFSSLKey
r.Timestamp = req.Timestamp

j, err := json.Marshal(r)
Expand All @@ -142,6 +138,28 @@ func GenerateCert(req *GenerateRequest, remoteAddr string) (cert string, key str
return
}

func SignOCSPResponse(req *OCSPSignRequest) (resp string, err error) {
if req.Certificate == "" {
err = errors.New("Invalid certificate sent")
return
}
j, err := json.Marshal(req)
if err != nil {
return
}
res, err := request("/api/v1/cfssl/ocspsign", j)
if err != nil {
return
}

var ok bool
if resp, ok = res.Result["ocspResponse"].(string); !ok {
err = fmt.Errorf("Missing certificate from ocspsign req %v", res.Result)
return
}
return
}

func request(path string, req []byte) (*cfsslResult, error) {
url := config.CFSSLAddr + path
if matched, _ := regexp.Match("/^https?://.*", []byte(url)); !matched {
Expand Down
46 changes: 37 additions & 9 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
// packages.
package config

import "github.com/mediocregopher/lever"
import (
"github.com/mediocregopher/lever"
)

// Configurable variables which are made available
var (
InternalAPIAddr string
HMACKey []byte
HMACAdminKey []byte
OVPNTemplateFile string
CAFile string
CFSSLAddr string
CFSSLKey string
DefaultNameFile string
TimestampDrift float64
CertsFile string
OCSPRespFile string
AutoOCSP bool
)

func init() {
Expand All @@ -26,7 +31,12 @@ func init() {
})
l.Add(lever.Param{
Name: "--key",
Description: "HMAC key for incoming requests",
Description: "HMAC key for incoming requests to /generate",
Default: "",
})
l.Add(lever.Param{
Name: "--admin-key",
Description: "HMAC key for incoming requests to /revoke /list (defaults to --key)",
Default: "",
})
l.Add(lever.Param{
Expand All @@ -44,11 +54,6 @@ func init() {
Description: "Address to cfssl server",
Default: "127.0.0.1:8888",
})
l.Add(lever.Param{
Name: "--cfssl-auth",
Description: "Auth Key to create certificates in cfssl",
Default: "",
})
l.Add(lever.Param{
Name: "--default-name-file",
Description: "Default name params in a json file",
Expand All @@ -59,16 +64,39 @@ func init() {
Description: "Maximum allowed timestamp drift in seconds (0 to disable checking)",
Default: "10",
})
l.Add(lever.Param{
Name: "--certs-file",
Description: "Save/Read certificates to/from a file",
Default: "",
})
l.Add(lever.Param{
Name: "--ocsp-responses-file",
Description: "OCSP responses file to serve from and update",
Default: "",
})
l.Add(lever.Param{
Name: "--auto-ocsp-sign",
Description: "Automatically OCSP sign new certificates",
Flag: true,
})
l.Parse()

InternalAPIAddr, _ = l.ParamStr("--internal-addr")
k, _ := l.ParamStr("--key")
HMACKey = []byte(k)
HMACAdminKey = HMACKey
k, _ = l.ParamStr("--admin-key")
if len(k) > 0 {
HMACAdminKey = []byte(k)
}
OVPNTemplateFile, _ = l.ParamStr("--ovpn-template")
CAFile, _ = l.ParamStr("--ca-file")
CFSSLAddr, _ = l.ParamStr("--cfssl-addr")
CFSSLKey, _ = l.ParamStr("--cfssl-auth")
DefaultNameFile, _ = l.ParamStr("--default-name-file")
td, _ := l.ParamInt("--timestamp-drift")
TimestampDrift = float64(td)
CertsFile, _ = l.ParamStr("--certs-file")
OCSPRespFile, _ = l.ParamStr("--ocsp-responses-file")
AutoOCSP = l.ParamFlag("--auto-ocsp-sign")

}
Loading