mirror of
https://github.com/outbackdingo/matchbox.git
synced 2026-01-27 10:19:35 +00:00
api: Add OpenPGP signature endpoints (.sig)
* Compliment the ASCII armored signature endpoints (.asc)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,4 +28,5 @@ _testmain.go
|
||||
|
||||
bin/
|
||||
assets/
|
||||
*.aci
|
||||
*.aci
|
||||
trustdb.gpg
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.**
|
||||
|
||||
|
||||
@@ -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()))))
|
||||
|
||||
@@ -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
0
sign/fixtures/empty.gpg
Normal file
BIN
sign/fixtures/mangled.gpg
Normal file
BIN
sign/fixtures/mangled.gpg
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user