diff --git a/.gitignore b/.gitignore index 6d85bbf3..6512e9b5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ _testmain.go bin/ assets/ -*.aci \ No newline at end of file +*.aci +trustdb.gpg \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 8c86cfa2..8b6ffc43 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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` diff --git a/Documentation/api.md b/Documentation/api.md index 7d3caae4..d835a099 100644 --- a/Documentation/api.md +++ b/Documentation/api.md @@ -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 diff --git a/Documentation/openpgp.md b/Documentation/openpgp.md index 6f868336..c86a0513 100644 --- a/Documentation/openpgp.md +++ b/Documentation/openpgp.md @@ -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.** diff --git a/api/server.go b/api/server.go index b16f3dd6..d58e950c 100644 --- a/api/server.go +++ b/api/server.go @@ -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())))) diff --git a/cmd/bootcfg/main.go b/cmd/bootcfg/main.go index 8ff1c46e..edf6a482 100644 --- a/cmd/bootcfg/main.go +++ b/cmd/bootcfg/main.go @@ -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) diff --git a/sign/fixtures/empty.gpg b/sign/fixtures/empty.gpg new file mode 100644 index 00000000..e69de29b diff --git a/sign/fixtures/mangled.gpg b/sign/fixtures/mangled.gpg new file mode 100644 index 00000000..35ed6bf1 Binary files /dev/null and b/sign/fixtures/mangled.gpg differ diff --git a/sign/signer.go b/sign/signer.go index e2ed2516..f6d38c8a 100644 --- a/sign/signer.go +++ b/sign/signer.go @@ -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 diff --git a/sign/signer_test.go b/sign/signer_test.go index b0523b40..8e1902b6 100644 --- a/sign/signer_test.go +++ b/sign/signer_test.go @@ -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) -} diff --git a/sign/writer_test.go b/sign/writer_test.go index fb7d5d7d..99204f0d 100644 --- a/sign/writer_test.go +++ b/sign/writer_test.go @@ -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) +}