api: Add OpenPGP signature endpoints (.sig)

* Compliment the ASCII armored signature endpoints (.asc)
This commit is contained in:
Dalton Hubble
2016-02-10 21:08:20 -08:00
parent 35f24eb720
commit 1ae5d113eb
11 changed files with 176 additions and 98 deletions

3
.gitignore vendored
View File

@@ -28,4 +28,5 @@ _testmain.go
bin/
assets/
*.aci
*.aci
trustdb.gpg

View File

@@ -2,13 +2,15 @@
## Latest
* Add detached OpenPGP signature endpoints (`.sig`)
## v0.2.0 (2016-02-09)
#### Features
* Render Ignition config and cloud-configs as Go templates
* Allow writing Ignition configs as YAML configs. Render as JSON for machines.
* Add detached OpenPGP signature endpoints (`.asc`) for all configs.
* Add ASCII armored detached OpenPGP signature endpoints (`.asc`)
- Enable signing by providing a `-key-ring-path` with a signing key and setting `BOOTCFG_PASSPHRASE` if needed
* Add `metadata` endpoint which matches machines to custom metadata
* Add `metadata` to group definitions in `config.yaml`

View File

@@ -112,17 +112,17 @@ Finds the spec matching the attribute query parameters and renders the correspon
## OpenPGP Signatures
OpenPGPG signature endpoints serve ASCII armored detached signatures of rendered configs when signing is enabled. See [OpenPGP Signing](openpgp.md).
OpenPGPG signature endpoints serve detached binary and ASCII armored signatures of rendered configs when signing is enabled. See [OpenPGP Signing](openpgp.md).
| Endpoint | ASCII Signature Endpoint |
|------------|-----------------|
| Ignition | `http://bootcfg.foo/ignition.asc` |
| Cloud-init | `http://bootcfg.foo/cloud.asc` |
| iPXE | `http://bootcfg.foo/boot.ipxe.asc` |
| iPXE | `http://bootcfg.foo/ipxe.asc` |
| Pixiecore | `http://bootcfg.foo/pixiecore/v1/boot.asc/:MAC` |
| Endpoint | Signature Endpoint | ASCII Signature Endpoint |
|------------|--------------------|-------------------------|
| Ignition | `http://bootcfg.foo/ignition.sig` | `http://bootcfg.foo/ignition.asc` |
| Cloud-init | `http://bootcfg.foo/cloud.sig` | `http://bootcfg.foo/cloud.asc` |
| iPXE | `http://bootcfg.foo/boot.ipxe.sig` | `http://bootcfg.foo/boot.ipxe.asc` |
| iPXE | `http://bootcfg.foo/ipxe.sig` | `http://bootcfg.foo/ipxe.asc` |
| Pixiecore | `http://bootcfg/pixiecore/v1/boot.sig/:MAC` | `http://bootcfg/pixiecore/v1/boot.asc/:MAC` |
Get an Ignition config and its detached signature.
Get an Ignition config and its detached ASCII armored signature.
GET http://bootcfg.foo/ipxe?attribute=value

View File

@@ -1,21 +1,21 @@
# OpenPGP Signing
The `bootcfg` OpenPGP signature endpoints serve ASCII armored detached signatures of rendered configs, if enabled. Each config endpoint has a corresponding signature endpoint, usually suffixed by `.asc`.
The `bootcfg` OpenPGP signature endpoints serve detached binary and ASCII armored signatures of rendered configs, if enabled. Each config endpoint has corresponding signature endpoints, typically suffixed with `.sig` or `.asc`.
To enable OpenPGP signing, provide the path to a secret keyring containing a single signing key with `-key-ring-path` or by setting `BOOTCFG_KEY_RING_PATH`. If a passphrase is required, set it via the `BOOTCFG_PASSPHRASE` environment variable.
Here are example signature endpoints without their query parameters.
| Endpoint | ASCII Signature Endpoint |
|------------|-----------------|
| Ignition | `http://bootcfg.foo/ignition.asc` |
| Cloud-init | `http://bootcfg.foo/cloud.asc` |
| iPXE | `http://bootcfg.foo/boot.ipxe.asc` |
| iPXE | `http://bootcfg.foo/ipxe.asc` |
| Pixiecore | `http://bootcfg.foo/pixiecore/v1/boot.asc/:MAC` |
| Endpoint | Signature Endpoint | ASCII Signature Endpoint |
|------------|--------------------|-------------------------|
| Ignition | `http://bootcfg.foo/ignition.sig` | `http://bootcfg.foo/ignition.asc` |
| Cloud-init | `http://bootcfg.foo/cloud.sig` | `http://bootcfg.foo/cloud.asc` |
| iPXE | `http://bootcfg.foo/boot.ipxe.sig` | `http://bootcfg.foo/boot.ipxe.asc` |
| iPXE | `http://bootcfg.foo/ipxe.sig` | `http://bootcfg.foo/ipxe.asc` |
| Pixiecore | `http://bootcfg/pixiecore/v1/boot.sig/:MAC` | `http://bootcfg/pixiecore/v1/boot.asc/:MAC` |
In production, mount your signing keyring and source the passphrase from a [Kubernetes secret](http://kubernetes.io/v1.1/docs/user-guide/secrets.html). Use a signing subkey exported to a keyring used only for config signing, which can be revoked by a master if needed.
In production, mount your signing keyring and source the passphrase from a [Kubernetes secret](http://kubernetes.io/v1.1/docs/user-guide/secrets.html). Use a signing subkey exported to a keyring by itself, which can be revoked by a primary key, if needed.
To try it locally, you may use the test fixture keyring. **Warning: The test fixture keyring is for examples only.**

View File

@@ -20,23 +20,26 @@ type Config struct {
Store Store
// Path to static assets
AssetsPath string
// Config signer
Signer sign.Signer
// config signers (.sig and .asc)
Signer sign.Signer
ArmoredSigner sign.Signer
}
// Server serves matches boot and configuration settings to machines.
// Server serves boot and provisioning configs to machines.
type Server struct {
store Store
assetsPath string
signer sign.Signer
store Store
assetsPath string
signer sign.Signer
armoredSigner sign.Signer
}
// NewServer returns a new Server.
func NewServer(config *Config) *Server {
return &Server{
store: config.Store,
assetsPath: config.AssetsPath,
signer: config.Signer,
store: config.Store,
assetsPath: config.AssetsPath,
signer: config.Signer,
armoredSigner: config.ArmoredSigner,
}
}
@@ -61,11 +64,23 @@ func (s *Server) HTTPHandler() http.Handler {
// metadata
mux.Handle("/metadata", logRequests(NewHandler(gr.matchGroupHandler(metadataHandler()))))
// Signatures
signerChain := func(next http.Handler) http.Handler {
return logRequests(sign.SignatureHandler(s.signer, next))
}
// Singatures
if s.signer != nil {
signerChain := func(next http.Handler) http.Handler {
return logRequests(sign.SignatureHandler(s.signer, next))
}
mux.Handle("/boot.ipxe.sig", signerChain(ipxeInspect()))
mux.Handle("/boot.ipxe.0.sig", signerChain(ipxeInspect()))
mux.Handle("/ipxe.sig", signerChain(NewHandler(gr.matchSpecHandler(ipxeHandler()))))
mux.Handle("/pixiecore/v1/boot.sig/", signerChain(pixiecoreHandler(gr, s.store)))
mux.Handle("/cloud.sig", signerChain(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
mux.Handle("/ignition.sig", signerChain(NewHandler(gr.matchGroupHandler(ignitionHandler(s.store)))))
mux.Handle("/metadata.sig", signerChain(NewHandler(gr.matchGroupHandler(metadataHandler()))))
}
if s.armoredSigner != nil {
signerChain := func(next http.Handler) http.Handler {
return logRequests(sign.SignatureHandler(s.armoredSigner, next))
}
mux.Handle("/boot.ipxe.asc", signerChain(ipxeInspect()))
mux.Handle("/boot.ipxe.0.asc", signerChain(ipxeInspect()))
mux.Handle("/ipxe.asc", signerChain(NewHandler(gr.matchSpecHandler(ipxeHandler()))))

View File

@@ -87,13 +87,14 @@ func main() {
store := api.NewFileStore(http.Dir(flags.dataPath))
// (optional) signing
var signer sign.Signer
var signer, armoredSigner sign.Signer
if flags.keyRingPath != "" {
var err error
signer, err = sign.LoadGPGSigner(flags.keyRingPath, passphrase)
entity, err := sign.LoadGPGEntity(flags.keyRingPath, passphrase)
if err != nil {
log.Fatal(err)
}
signer = sign.NewGPGSigner(entity)
armoredSigner = sign.NewArmoredGPGSigner(entity)
}
// load bootstrap config
@@ -105,9 +106,10 @@ func main() {
// API server
config := &api.Config{
Store: store,
AssetsPath: flags.assetsPath,
Signer: signer,
Store: store,
AssetsPath: flags.assetsPath,
Signer: signer,
ArmoredSigner: armoredSigner,
}
server := api.NewServer(config)
log.Infof("starting config server on %s", flags.address)

0
sign/fixtures/empty.gpg Normal file
View File

BIN
sign/fixtures/mangled.gpg Normal file

Binary file not shown.

View File

@@ -18,29 +18,47 @@ type Signer interface {
Sign(w io.Writer, message io.Reader) error
}
// gpgSigner reads messages and writes ascii armored OpenPGP signatures.
// gpgSigner reads messages and writes OpenPGP signatures.
type gpgSigner struct {
signer *openpgp.Entity
}
// Sign signs the given message and writes the ascii armored OpenPGP signature
// Sign signs the given message and writes the detached OpenPGP signature
// to w.
func (s *gpgSigner) Sign(w io.Writer, message io.Reader) error {
return openpgp.ArmoredDetachSignText(w, s.signer, message, nil)
return openpgp.DetachSignText(w, s.signer, message, nil)
}
// NewGPGSigner returns a new Signer that reads messages and writes ascii
// armored OpenPGP signatures.
// NewGPGSigner returns a new Signer that reads messages and writes OpenPGP
// signatures.
func NewGPGSigner(signer *openpgp.Entity) Signer {
return &gpgSigner{
signer: signer,
}
}
// LoadGPGSigner loads a key ring file, unlocks the first key with the given
// passphrase, and returns a new Signer that reads messages and writes ascii
// armored OpenPGP signatures.
func LoadGPGSigner(keyRingPath, passphrase string) (Signer, error) {
// armoredGPGSigner reads messages and writes ascii armored OpenPGP signatures.
type armoredGPGSigner struct {
signer *openpgp.Entity
}
// Sign signs the given message and writes the detached ascii armored OpenPGP
// signature to w.
func (s *armoredGPGSigner) Sign(w io.Writer, message io.Reader) error {
return openpgp.ArmoredDetachSignText(w, s.signer, message, nil)
}
// NewArmoredGPGSigner returns a new Signer that reads messages and writes
// ascii armored OpenPGP signatures.
func NewArmoredGPGSigner(signer *openpgp.Entity) Signer {
return &armoredGPGSigner{
signer: signer,
}
}
// LoadGPGEntity loads a key ring file, unlocks the first key using the given
// passphrase, and returns a new OpenPGP Entity for signing.
func LoadGPGEntity(keyRingPath, passphrase string) (*openpgp.Entity, error) {
kring, err := os.Open(keyRingPath)
if err != nil {
return nil, err
@@ -50,7 +68,7 @@ func LoadGPGSigner(keyRingPath, passphrase string) (Signer, error) {
if err != nil {
return nil, err
}
return NewGPGSigner(entity), nil
return entity, nil
}
// unlockKeyRingEntity loads a key ring file and returns the first Entity. The

View File

@@ -2,10 +2,6 @@ package sign
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"testing"
@@ -14,20 +10,76 @@ import (
"golang.org/x/crypto/openpgp"
)
func TestLoadGPGSigner(t *testing.T) {
signer, err := LoadGPGSigner("fixtures/secring.gpg", "test")
func TestLoadGPGEntity(t *testing.T) {
entity, err := LoadGPGEntity("fixtures/secring.gpg", "test")
assert.Nil(t, err)
assert.NotNil(t, entity)
}
func TestLoadGPGEntity_MissingKeyring(t *testing.T) {
_, err := LoadGPGEntity("", "")
assert.NotNil(t, err)
}
func TestLoadGPGEntity_ReadKeyringError(t *testing.T) {
_, err := LoadGPGEntity("fixtures/mangled.gpg", "test")
assert.NotNil(t, err)
}
func TestLoadGPGEntity_EmptyKeyring(t *testing.T) {
_, err := LoadGPGEntity("fixtures/empty.gpg", "")
assert.Equal(t, errEmptyKeyring, err)
}
func TestLoadGPGEntity_MissingPassphrase(t *testing.T) {
_, err := LoadGPGEntity("fixtures/secring.gpg", "")
assert.Equal(t, errMissingPassphrase, err)
}
func TestLoadGPGEntity_IncorrectPassphrase(t *testing.T) {
_, err := LoadGPGEntity("fixtures/secring.gpg", "incorrect")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "private key checksum failure")
}
}
func TestGPGSigner(t *testing.T) {
entity, err := LoadGPGEntity("fixtures/secring.gpg", "test")
assert.Nil(t, err)
// assert that:
// - fixture private key is read from a key ring file
// - fixture encrypted private key is decrypted by passphrase
// - Signer signs a message which can be verified by OpenPGP
// - gpgSigner creates a signature which can be verified
signer := NewGPGSigner(entity)
expectedMessage := "Hello World!"
signature := new(bytes.Buffer)
fmt.Println(signature)
err = signer.Sign(signature, strings.NewReader(expectedMessage))
assert.Nil(t, err)
// valid signature
// gpg --no-default-keyring --secret-keyring fixtures/secring.gpg --verify sig msg
// gpg --homedir sign/fixtures --verify sig msg
kring, err := os.Open("fixtures/secring.gpg")
assert.Nil(t, err)
defer kring.Close()
entities, err := openpgp.ReadKeyRing(kring)
assert.Nil(t, err)
_, err = openpgp.CheckDetachedSignature(entities, strings.NewReader(expectedMessage), signature)
assert.Nil(t, err)
}
func TestArmoredGPGSigner(t *testing.T) {
entity, err := LoadGPGEntity("fixtures/secring.gpg", "test")
assert.Nil(t, err)
// assert that:
// - fixture private key is read from a key ring file
// - fixture encrypted private key is decrypted by passphrase
// - armoredGPGSigner creates an armored signature which can be verified
signer := NewArmoredGPGSigner(entity)
expectedMessage := "Hello World!"
signature := new(bytes.Buffer)
err = signer.Sign(signature, strings.NewReader(expectedMessage))
assert.Nil(t, err)
// valid signature
// gpg --homedir sign/fixtures --verify sig msg
kring, err := os.Open("fixtures/secring.gpg")
assert.Nil(t, err)
defer kring.Close()
@@ -36,43 +88,3 @@ func TestLoadGPGSigner(t *testing.T) {
_, err = openpgp.CheckArmoredDetachedSignature(entities, strings.NewReader(expectedMessage), signature)
assert.Nil(t, err)
}
func TestLoadGPGSigner_MissingKeyRing(t *testing.T) {
_, err := LoadGPGSigner("", "")
assert.NotNil(t, err)
}
func TestLoadGPGSigner_MissingPassphrase(t *testing.T) {
_, err := LoadGPGSigner("fixtures/secring.gpg", "")
assert.Equal(t, errMissingPassphrase, err)
}
func TestLoadGPGSigner_IncorrectPassphrase(t *testing.T) {
_, err := LoadGPGSigner("fixtures/secring.gpg", "incorrect")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "private key checksum failure")
}
}
// upperSigner "signs" messages by writing a signature that is the upper case
// form of the message body. For testing purposes only.
type upperSigner struct{}
func (s *upperSigner) Sign(w io.Writer, message io.Reader) error {
b, err := ioutil.ReadAll(message)
if err != nil {
return err
}
signature := strings.ToUpper(string(b))
_, err = io.Copy(w, bytes.NewReader([]byte(signature)))
return err
}
// errorSigner always returns an error message.
type errorSigner struct {
errorMessage string
}
func (s *errorSigner) Sign(w io.Writer, message io.Reader) error {
return errors.New(s.errorMessage)
}

View File

@@ -1,9 +1,14 @@
package sign
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -64,3 +69,26 @@ func TestSignatureHandler_SignatureError(t *testing.T) {
assert.Equal(t, errorMessage+"\n", w.Body.String())
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
// upperSigner "signs" messages by writing a signature that is the upper case
// form of the message body. For testing purposes only.
type upperSigner struct{}
func (s *upperSigner) Sign(w io.Writer, message io.Reader) error {
b, err := ioutil.ReadAll(message)
if err != nil {
return err
}
signature := strings.ToUpper(string(b))
_, err = io.Copy(w, bytes.NewReader([]byte(signature)))
return err
}
// errorSigner always returns an error message.
type errorSigner struct {
errorMessage string
}
func (s *errorSigner) Sign(w io.Writer, message io.Reader) error {
return errors.New(s.errorMessage)
}