diff --git a/README.md b/README.md index 42919460..dff3dc7e 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ $ curl \ https://vault.example.com/v1/gpg/keys/my-imported-key ``` -### Read key +### Read key by name This endpoint returns information about a named GPG key. @@ -117,6 +117,39 @@ $ curl \ #### Sample response +```json +{ + "data": { + "exportable": false, + "fingerprint": "b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d", + "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxsBNBFmZ6QQBCAC5QSHMKe6M9S2G9REo3sJuDPX2lm4ZMULXCvwcVekPYyUFWYI8\n...\nnTruSryJ4xYCydiJ1xkTedrkVxhh7hJKHA==\n=4fdy\n-----END PGP PUBLIC KEY BLOCK-----" + } +} +``` + +### Read key by fingerprint + +This endpoint returns information about a key associated with the specified fingerprint. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `GET` | `/gpg/keys/id/:ID` | `200 application/json` | + +#### Parameters + +- `ID` `(string: )` – Specifies the fingerprint of the key to read. This is specified as part of the URL. + +#### Sample request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + https://vault.example.com/v1/gpg/keys/id/b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d +``` + +#### Sample response + + ```json { "data": { @@ -175,6 +208,138 @@ $ curl \ https://vault.example.com/v1/gpg/keys/my-key ``` +### Create subkey + +This endpoint creates a new RSA gpg subkey associated with a named key. + +| Method | Path | Produces | +| :------- | :------------------------------ | :---------------------- | +| `POST` | `/gpg/subkeys/:name` | `200 application/json` | + +#### Parameters + +- `name` `(string: )` – Specifies the name of the key to which subkey will be added. This is specified as part of the URL. + +- `key_bits` `(int: 2048)` – Specifies the number of bits of the generated GPG subkey to use. + +- `canSign` `(bool: false)` – Specifies if the subkey can be used for signing. + +- `canEncrypt` `(bool: true)` – Specifies if the subkey can be used for encryption. + +#### Sample Payload + +```json +{ + "canSign": true, + "canEncrypt": true, + "key_bits": 4096 +} +``` + +#### Sample request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.example.com/v1/gpg/subkeys/my-key +``` + +#### Sample response + +```json +{ + "data": { + "subkey-id": "b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d", + "name": "my-key", + } +} +``` + +### Read subkey by fingerprint + +This endpoint returns information about a specific subkey of a named key referenced using it's fingerprint. + +| Method | Path | Produces | +| :------- | :----------------------------- | :--------------------- | +| `GET` | `/gpg/subkeys/:name/:subkeyID` | `200 application/json` | + +#### Parameters + +- `name` `(string: )` – Specifies the name of the key to read. This is specified as part of the URL. + +- `subkeyID` `(string: )` – Specifies the fingerprint of the subkey to read. This is specified as part of the URL. + +#### Sample request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + https://vault.example.com/v1/gpg/keys/my-key/b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d +``` + +#### Sample response + +```json +{ + "data": { + "exportable": false, + "subkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxsBNBFmZ6QQBCAC5QSHMKe6M9S2G9REo3sJuDPX2lm4ZMULXCvwcVekPYyUFWYI8\n...\nnTruSryJ4xYCydiJ1xkTedrkVxhh7hJKHA==\n=4fdy\n-----END PGP PUBLIC KEY BLOCK-----" + } +} +``` + +### List keys + +This endpoint returns a list of subkeys of a named GPG key. Only the fingerprint of the subkeys are returned. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `LIST` | `/gpg/subkeys/:name` | `200 application/json` | + +#### Sample request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + https://vault.example.com/v1/gpg/subkeys/my-key +``` + +#### Sample response + +```json +{ + "data": { + "keys": ["b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d", "20b7e7fa124ba1a631d15196ef3331150a45bc4d"] + } +} +``` + +### Delete subkey + +This endpoint deletes a subkey of a named GPG key referenced using it's fingerprint. + +| Method | Path | Produces | +| :------- | :------------------------------ | :--------------------- | +| `DELETE` | `/gpg/subkeys/:name/:subkeyID` | `204 (empty body)` | + +#### Parameters + +- `name` `(string: )` – Specifies the name of the key to delete. This is specified as part of the URL. + +- `subkeyID` `(string: )` – Specifies the fingerprint of the subkey to read. This is specified as part of the URL. + +#### Sample request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request DELETE \ + https://vault.example.com/v1/gpg/subkeys/my-key/b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d +``` + ### Export key This endpoint returns the named GPG key ASCII-armored. @@ -419,3 +584,82 @@ $ curl \ } } ``` + +### Revoke key or subkey + +This endpoint revokes a named key or a specific subkey of a named key. + +| Method | Path | Produces | +| :------- | :------------------------------ | :---------------------- | +| `POST` | `/gpg/revoke/:name/:subkeyID` | `204 (empty body)` | + +#### Parameters + +- `name` `(string: )` – Specifies the name of the key to which subkey will ne added. This is specified as part of the URL. + +- `subkeyID` `(string: "")` – Specifies the fingerprint of the subkey to be revoked. This is optional and specified as a part of the URL.. + +- `reasonCode` `(int: 2048)` – Specifies the uint8 reason code for key revocation as per RFC4880. + +- `reasonText` `(string: "")` – Specifies the comment associated with the reason for revoking the key/subkey. + +#### Sample Payload + +```json +{ + "reasonCode": 2, + "reasonText": "Key is compromised.", +} +``` + +#### Sample request for revoking key + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.example.com/v1/gpg/revoke/my-key +``` + +#### Sample request for revoking subkey + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.example.com/v1/gpg/revoke/my-key/b0b7e7ca0e4ba1a631d15196ef3331150a45bc4d +``` + +### Sign a key stored in vault + +This endpoint signs a key stored on vault with another key to show trust in the signed key. + +| Method | Path | Produces | +| :------- | :------------------------------ | :---------------------- | +| `POST` | `/gpg/signKey/:signedKey` | `204 (empty body)` | + +#### Parameters + +- `name` `(string: )` – Specifies the name of the key that will be used for signing. + +- `signedKey` `(string: "")` – Specifies the name of the key that needs to be signed. This is specified as a part of the URL. + +#### Sample Payload + +```json +{ + "name": "signer-key", +} +``` + +#### Sample request for signing a key + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.example.com/v1/gpg/signkey/my-key +``` diff --git a/go.mod b/go.mod index 5296bb73..d3a81826 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,5 @@ require ( github.com/hashicorp/vault/sdk v0.1.10 golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c ) + +replace golang.org/x/crypto => github.com/syadav2015/crypto v0.0.0-20190304101048-6881110aac18 diff --git a/go.sum b/go.sum index 857aff34..8d4208d0 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/syadav2015/crypto v0.0.0-20190304101048-6881110aac18 h1:j7cBfzrR6zkHXRzekBcD9SdKGL1/BdQ9MBJivqHqMrA= +github.com/syadav2015/crypto v0.0.0-20190304101048-6881110aac18/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/gpg/backend.go b/gpg/backend.go index 8f796870..3e7a4682 100644 --- a/gpg/backend.go +++ b/gpg/backend.go @@ -2,6 +2,7 @@ package gpg import ( "context" + "sync" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" @@ -23,9 +24,14 @@ func Backend() *backend { Help: backendHelp, Paths: []*framework.Path{ pathKeys(&b), + pathKeysByFingerprint(&b), pathListKeys(&b), + pathListSubkeys(&b), + pathRevoke(&b), + pathSubkeys(&b), pathExportKeys(&b), pathSign(&b), + pathSignKey(&b), pathVerify(&b), pathDecrypt(&b), pathShowSessionKey(&b), @@ -43,6 +49,7 @@ func Backend() *backend { type backend struct { *framework.Backend + lock sync.RWMutex } const backendHelp = ` diff --git a/gpg/path_decrypt.go b/gpg/path_decrypt.go index 41c1133f..864a3834 100644 --- a/gpg/path_decrypt.go +++ b/gpg/path_decrypt.go @@ -54,7 +54,10 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, da return logical.ErrorResponse(fmt.Sprintf("unsupported encoding format %s; must be \"base64\" or \"ascii-armor\"", format)), nil } + // Acquire a read lock before the read operation. + b.lock.RLock() keyEntry, err := b.key(ctx, req.Storage, data.Get("name").(string)) + b.lock.RUnlock() if err != nil { return nil, err } diff --git a/gpg/path_export.go b/gpg/path_export.go index a1e97412..c8c13409 100644 --- a/gpg/path_export.go +++ b/gpg/path_export.go @@ -30,7 +30,10 @@ func pathExportKeys(b *backend) *framework.Path { func (b *backend) pathExportKeyRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) + // Acquire a read lock before the read operation. + b.lock.RLock() entry, err := b.key(ctx, req.Storage, name) + b.lock.RUnlock() if err != nil { return nil, err } diff --git a/gpg/path_keys.go b/gpg/path_keys.go index 85532678..b44ba2c0 100644 --- a/gpg/path_keys.go +++ b/gpg/path_keys.go @@ -4,14 +4,13 @@ import ( "bytes" "context" "encoding/hex" - "fmt" + "strings" + "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" - "io" - "strings" ) func pathListKeys(b *backend) *framework.Path { @@ -109,63 +108,36 @@ func (b *backend) entity(entry *keyEntry) (*openpgp.Entity, error) { return el[0], nil } -func serializePrivateWithoutSigning(w io.Writer, e *openpgp.Entity) (err error) { - foundPrivateKey := false - - if e.PrivateKey != nil { - foundPrivateKey = true - err = e.PrivateKey.Serialize(w) - if err != nil { - return - } - } - for _, ident := range e.Identities { - err = ident.UserId.Serialize(w) - if err != nil { - return - } - err = ident.SelfSignature.Serialize(w) - if err != nil { - return - } +func (b *backend) readKeyByName(ctx context.Context, req *logical.Request, name string) (*openpgp.Entity, bool, error) { + // Acquire a read lock before the read operation. + b.lock.RLock() + entry, err := b.key(ctx, req.Storage, name) + b.lock.RUnlock() + if err != nil { + return nil, false, err } - for _, subkey := range e.Subkeys { - if subkey.PrivateKey != nil { - foundPrivateKey = true - err = subkey.PrivateKey.Serialize(w) - if err != nil { - return - } - } - err = subkey.Sig.Serialize(w) - if err != nil { - return - } + if entry == nil { + return nil, false, nil } - - if !foundPrivateKey { - return fmt.Errorf("No private key has been found") + entity, err := b.entity(entry) + if err != nil { + return nil, false, err } - - return nil + return entity, entry.Exportable, nil } func (b *backend) pathKeyRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - entry, err := b.key(ctx, req.Storage, data.Get("name").(string)) + entity, exportable, err := b.readKeyByName(ctx, req, data.Get("name").(string)) if err != nil { return nil, err } - if entry == nil { + if entity == nil { return nil, nil } - entity, err := b.entity(entry) - if err != nil { - return nil, err - } var buf bytes.Buffer w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) - err = entity.Serialize(w) + err = serializeWithRevocations(w, entity) w.Close() if err != nil { return nil, err @@ -175,7 +147,7 @@ func (b *backend) pathKeyRead(ctx context.Context, req *logical.Request, data *f Data: map[string]interface{}{ "fingerprint": hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]), "public_key": buf.String(), - "exportable": entry.Exportable, + "exportable": exportable, }, }, nil } @@ -191,6 +163,7 @@ func (b *backend) pathKeyCreate(ctx context.Context, req *logical.Request, data key := data.Get("key").(string) var buf bytes.Buffer + var keyID string switch generate { case true: if keyBits < 2048 { @@ -207,6 +180,7 @@ func (b *backend) pathKeyCreate(ctx context.Context, req *logical.Request, data if err != nil { return nil, err } + keyID = hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]) default: if key == "" { return logical.ErrorResponse("the key value is required for generated keys"), nil @@ -215,10 +189,11 @@ func (b *backend) pathKeyCreate(ctx context.Context, req *logical.Request, data if err != nil { return logical.ErrorResponse(err.Error()), nil } - err = serializePrivateWithoutSigning(&buf, el[0]) + err = serializeEntityWithAllSignatures(&buf, el[0]) if err != nil { return logical.ErrorResponse("the key could not be serialized, is a private key present?"), nil } + keyID = hex.EncodeToString(el[0].PrimaryKey.Fingerprint[:]) } entry, err := logical.StorageEntryJSON("key/"+name, &keyEntry{ @@ -228,13 +203,34 @@ func (b *backend) pathKeyCreate(ctx context.Context, req *logical.Request, data if err != nil { return nil, err } + + keyIDToNameMap, err := b.readKeyIDToNameMap(ctx, req.Storage) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + // Acquire a write lock before writing the key. + b.lock.Lock() if err := req.Storage.Put(ctx, entry); err != nil { return nil, err } + b.lock.Unlock() + + if keyIDToNameMap == nil { + keyIDToNameMap = make(map[string]string) + } + keyIDToNameMap[keyID] = name + err = b.writeKeyIDToNameMap(ctx, req.Storage, keyIDToNameMap) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } return nil, nil } func (b *backend) pathKeyDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Acquire a write lock before deleting the key. + b.lock.Lock() + defer b.lock.Unlock() + err := req.Storage.Delete(ctx, "key/"+data.Get("name").(string)) if err != nil { return nil, err @@ -244,6 +240,10 @@ func (b *backend) pathKeyDelete(ctx context.Context, req *logical.Request, data func (b *backend) pathKeyList( ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + // Acquire a read lock before the read operation. + b.lock.RLock() + defer b.lock.RUnlock() + entries, err := req.Storage.List(ctx, "key/") if err != nil { return nil, err @@ -256,9 +256,9 @@ type keyEntry struct { Exportable bool } -const pathPolicyHelpSyn = "Managed named GPG keys" +const pathPolicyHelpSyn = "Managed named GPG keys and subkeys" const pathPolicyHelpDesc = ` -This path is used to manage the named GPG keys that are available. +This path is used to manage the named GPG keys and subkeys that are available. Doing a write with no value against a new named key will create it using a randomly generated key. ` diff --git a/gpg/path_keys_id.go b/gpg/path_keys_id.go new file mode 100644 index 00000000..75656888 --- /dev/null +++ b/gpg/path_keys_id.go @@ -0,0 +1,71 @@ +package gpg + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" +) + +// pathKeysByFingerprint allows a key to be queried by its fingerprint as querying keys by +// fingerprint is a common way of uniquely identifying gpg keys. +func pathKeysByFingerprint(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "keys/id/" + framework.GenericNameRegex("ID"), + Fields: map[string]*framework.FieldSchema{ + "ID": { + Type: framework.TypeString, + Description: "Fingerprint of the key.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathKeyByIDRead, + }, + }, + HelpSynopsis: pathPolicyHelpSyn, + HelpDescription: pathPolicyHelpDesc, + } +} + +func (b *backend) pathKeyByIDRead(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + // Verify and sanitize parameters. + id := data.Get("ID").(string) + + keyIDToNameMap, err := b.readKeyIDToNameMap(ctx, req.Storage) + if err != nil { + return nil, err + } + name, ok := keyIDToNameMap[id] + if !ok { + err = fmt.Errorf("Key with ID %s was not found", id) + return logical.ErrorResponse(err.Error()), err + } + entity, exportable, err := b.readKeyByName(ctx, req, name) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) + err = entity.Serialize(w) + w.Close() + if err != nil { + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "fingerprint": hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]), + "name": name, + "public_key": buf.String(), + "exportable": exportable, + }, + }, nil +} diff --git a/gpg/path_keys_id_test.go b/gpg/path_keys_id_test.go new file mode 100644 index 00000000..c4f59656 --- /dev/null +++ b/gpg/path_keys_id_test.go @@ -0,0 +1,80 @@ +package gpg + +import ( + "context" + "encoding/hex" + "fmt" + "testing" + + "github.com/hashicorp/vault/sdk/logical" +) + +func TestGPG_KeyIDNotSpecifiedReturnsError(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Key name is not specified. + req := &logical.Request{ + Storage: storage, + Operation: logical.ListOperation, + Path: "keys/id", + } + _, err := b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatalf("Key not specified but the API does not return an error") + } +} + +func TestGPG_KeyIDNotExistingReturnsNotFound(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Key does not exist. + req := &logical.Request{ + Storage: storage, + Operation: logical.ListOperation, + Path: "keys/id/FA129324743", + } + _, err := b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("Key does not exist but does not return not found") + } +} + +func TestGPG_PathKeysByID(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + testKeyName := "test" + err := addKeyToTestStorage(b, storage, testKeyName, false, gpgKey) + if err != nil { + t.Fatal(err) + } + + // Get subkey ID of the subkey. + entity, _, err := b.readEntityFromStorage(context.Background(), storage, "test") + if err != nil { + t.Fatalf("Failed to read key from storage after key revocation: %v", err) + } + + keyID := hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]) + + req := &logical.Request{ + Storage: storage, + Operation: logical.ReadOperation, + Path: fmt.Sprintf("keys/id/%s", keyID), + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("Failed to read a valid key by ID: %v", err) + } + + value, ok := resp.Data["name"] + if !ok { + t.Fatalf("no valid key name found in response data %#v", resp.Data) + } + if value != testKeyName { + t.Fatalf("Expected key name: %q, got: %q", testKeyName, value) + } +} diff --git a/gpg/path_keys_test.go b/gpg/path_keys_test.go index 462a119e..c2f61d88 100644 --- a/gpg/path_keys_test.go +++ b/gpg/path_keys_test.go @@ -100,6 +100,27 @@ func TestGPG_CreateErrorGeneratedKeyTooSmallKeyBits(t *testing.T) { } } +func TestGPG_ReadKeyByName(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgSignedAndRevokedTestKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.ReadOperation, + Path: "keys/test", + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("Key exists but failed to read: %v", err) + } +} + const gpgPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBFmZfJIBCACx2NgAf4rLLx2QKo444ATs3ewJICdy/cYhETxcn5wewdrxQayJ diff --git a/gpg/path_revoke.go b/gpg/path_revoke.go new file mode 100644 index 00000000..ff7c32d2 --- /dev/null +++ b/gpg/path_revoke.go @@ -0,0 +1,171 @@ +package gpg + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" + "golang.org/x/crypto/openpgp" +) + +type revocationParameters struct { + name string + subkeyID string + reasonCode uint8 + reasonText string +} + +func (rp *revocationParameters) parseInput(input *framework.FieldData) error { + var data interface{} + var ok bool + data, ok = input.GetOk("name") + if ok { + rp.name = data.(string) + } + + data, ok = input.GetOk("subkeyID") + if ok { + rp.subkeyID = data.(string) + } + + data, ok = input.GetOk("reasonCode") + if ok { + id, ok := data.(int) + if !ok { + return parameterTypeError("reasonCode", "int") + } + rp.reasonCode = uint8(id) + } else { + return fmt.Errorf("reasonCode cannot be empty") + } + + data, ok = input.GetOk("reasonText") + if ok { + rp.reasonText = data.(string) + } else { + return fmt.Errorf("reasonText cannot be empty") + } + + return nil +} + +func pathRevoke(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "revoke/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("subkeyID"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the key.", + }, + "subkeyID": { + Type: framework.TypeString, + Description: "Fingerprint of the subkey.", + }, + "reasonCode": { + Type: framework.TypeInt, + Description: "Reason code for key revocation.", + }, + "reasonText": { + Type: framework.TypeString, + Description: "Textual description of reason for key revocation.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathRevokeKey, + }, + }, + HelpSynopsis: pathRevokeHelpSyn, + HelpDescription: pathRevokeHelpDesc, + } +} + +func (b *backend) revokeKey(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var rp revocationParameters + err := rp.parseInput(data) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + // Acquire a write lock before modifying the entity. + b.lock.Lock() + defer b.lock.Unlock() + + entity, exportable, err := b.readEntityFromStorage(ctx, req.Storage, rp.name) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + err = entity.RevokeKey(rp.reasonCode, rp.reasonText, nil) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + err = b.writeEntityToStorage(ctx, req.Storage, rp.name, entity, exportable) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + return nil, nil +} + +func (b *backend) revokeSubkey(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var rp revocationParameters + err := rp.parseInput(data) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + // Acquire a write lock before modifying the entity. + b.lock.Lock() + defer b.lock.Unlock() + + entity, exportable, err := b.readEntityFromStorage(ctx, req.Storage, rp.name) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + var subkey *openpgp.Subkey + idx := 0 + for i, sk := range entity.Subkeys { + if rp.subkeyID == hex.EncodeToString(sk.PublicKey.Fingerprint[:]) { + subkey = &sk + idx = i + break + } + } + + if subkey == nil { + err = fmt.Errorf("Subkey with fingerprint %s for key %s was not found", rp.subkeyID, rp.name) + return logical.ErrorResponse(err.Error()), err + } + + err = entity.RevokeSubkey(subkey, rp.reasonCode, rp.reasonText, nil) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + entity.Subkeys[idx] = *subkey + + err = b.writeEntityToStorage(ctx, req.Storage, rp.name, entity, exportable) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + return nil, nil +} + +func (b *backend) pathRevokeKey(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + subkeyID := data.Get("subkeyID").(string) + if subkeyID == "" { + return b.revokeKey(ctx, req, data) + } else { + return b.revokeSubkey(ctx, req, data) + } +} + +const pathRevokeHelpSyn = "Revoke GPG keys and subkeys" +const pathRevokeHelpDesc = ` +This path is used to revoke GPG keys and subkeys that are available. +The updated keyring is stored back into vault. +` diff --git a/gpg/path_revoke_test.go b/gpg/path_revoke_test.go new file mode 100644 index 00000000..529c3938 --- /dev/null +++ b/gpg/path_revoke_test.go @@ -0,0 +1,205 @@ +package gpg + +import ( + "context" + "encoding/hex" + "fmt" + "testing" + + "github.com/hashicorp/vault/sdk/logical" +) + +const revocationReasonCode = 1 +const revocationReasonText = "Key compromised" + +func TestGPG_RevokeKeyNotExistingKeyReturnsNotFound(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "revoke/test", + Data: map[string]interface{}{ + "reasonCode": revocationReasonCode, + "reasonText": revocationReasonText, + }, + } + _, err := b.HandleRequest(context.Background(), req) + + if err == nil { + t.Fatal("Key does not exist but does not return not found") + } +} +func TestGPG_RevokeKeyReasonMissing(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "revoke/test", + } + _, err = b.HandleRequest(context.Background(), req) + + if err == nil { + t.Fatal("Key was revoked without a specified reason") + } +} + +func TestGPG_RevokeKeyRevocationReasonNotInteger(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "revoke/test/1a2frf", + Data: map[string]interface{}{ + "reasonCode": revocationReasonText, + "reasonText": revocationReasonText, + }, + } + _, err = b.HandleRequest(context.Background(), req) + + if err == nil { + t.Fatal("reasonCode is not an integer yet the key was revoked") + } +} + +func TestGPG_RevokeKeyNotExistingSubkeyReturnsNotFound(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "revoke/test/SubkeyDoesNotExist", + Data: map[string]interface{}{ + "reasonCode": revocationReasonCode, + "reasonText": revocationReasonText, + }, + } + _, err = b.HandleRequest(context.Background(), req) + + if err == nil { + t.Fatal("Subkey does not exist but does not return not found") + } +} + +func TestGPG_RevokeKey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Generate a new signed key. + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/test", + Data: map[string]interface{}{ + "generate": true, + "real_name": "test", + "email": "test@example.com", + "exportable": true, + }, + } + _, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + req = &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "revoke/test", + Data: map[string]interface{}{ + "reasonCode": revocationReasonCode, + "reasonText": revocationReasonText, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("Key revocation should be successful but it failed: %v", err) + } + entity, _, err := b.readEntityFromStorage(context.Background(), storage, "test") + if err != nil { + t.Fatalf("Failed to read key from storage after key revocation: %v", err) + } + if len(entity.Revocations) != 1 { + t.Fatal("Revocation signature missing from key after key revocation") + } +} + +func TestGPG_RevokeSubkey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Generate a new signed key. + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/test", + Data: map[string]interface{}{ + "generate": true, + "real_name": "test", + "email": "test@example.com", + "exportable": true, + }, + } + _, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + entity, _, err := b.readEntityFromStorage(context.Background(), storage, "test") + if err != nil { + t.Fatalf("Failed to read key from storage after key creation: %v", err) + } + subkeyID := hex.EncodeToString(entity.Subkeys[0].PublicKey.Fingerprint[:]) + req = &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("revoke/test/%s", subkeyID), + Data: map[string]interface{}{ + "reasonCode": revocationReasonCode, + "reasonText": revocationReasonText, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("Subkey revocation should be successful but it failed: %v", err) + } + + entity, _, err = b.readEntityFromStorage(context.Background(), storage, "test") + if err != nil { + t.Fatalf("Failed to read key from storage after key revocation: %v", err) + } + reasonCode := entity.Subkeys[0].Sig.RevocationReason + reasonText := entity.Subkeys[0].Sig.RevocationReasonText + if reasonCode == nil { + t.Fatal("Revocation signature missing from key after key revocation") + } + if *reasonCode != uint8(revocationReasonCode) { + t.Fatalf("Expected revocation reason code: %v, got: %v", revocationReasonCode, *reasonCode) + } + if reasonText != revocationReasonText { + t.Fatalf("Expected revocation reason text: %v, got: %v", revocationReasonText, reasonText) + } +} diff --git a/gpg/path_show_session_key.go b/gpg/path_show_session_key.go index 8d556a44..418feb4d 100644 --- a/gpg/path_show_session_key.go +++ b/gpg/path_show_session_key.go @@ -58,7 +58,10 @@ func (b *backend) pathShowSessionKeyWrite(ctx context.Context, req *logical.Requ return logical.ErrorResponse(fmt.Sprintf("unsupported encoding format %s; must be \"base64\" or \"ascii-armor\"", format)), nil } + // Acquire a read lock before the read operation. + b.lock.RLock() keyEntry, err := b.key(ctx, req.Storage, data.Get("name").(string)) + b.lock.RUnlock() if err != nil { return nil, err } diff --git a/gpg/path_sign_verify.go b/gpg/path_sign_verify.go index b60da2a2..024e54e0 100644 --- a/gpg/path_sign_verify.go +++ b/gpg/path_sign_verify.go @@ -123,7 +123,10 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, data return logical.ErrorResponse(fmt.Sprintf("unsupported encoding format %s; must be \"base64\" or \"ascii-armor\"", format)), nil } + // Acquire a read lock before the read operation. + b.lock.RLock() entry, err := b.key(ctx, req.Storage, data.Get("name").(string)) + b.lock.RUnlock() if err != nil { return nil, err } @@ -177,7 +180,10 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, dat return logical.ErrorResponse(fmt.Sprintf("unsupported encoding format %s; must be \"base64\" or \"ascii-armor\"", format)), nil } + // Acquire a read lock before the read operation. + b.lock.RLock() keyEntry, err := b.key(ctx, req.Storage, data.Get("name").(string)) + b.lock.RUnlock() if err != nil { return nil, err } diff --git a/gpg/path_signkey.go b/gpg/path_signkey.go new file mode 100644 index 00000000..40f15a1d --- /dev/null +++ b/gpg/path_signkey.go @@ -0,0 +1,98 @@ +package gpg + +import ( + "context" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +type signKeyParameters struct { + name string + signedKey string +} + +func (skp *signKeyParameters) parseInput(input *framework.FieldData) error { + var data interface{} + var ok bool + data, ok = input.GetOk("name") + if ok { + skp.name, ok = data.(string) + if !ok { + return parameterTypeError("name", "string") + } + } + + data, ok = input.GetOk("signedKey") + if ok { + skp.signedKey, ok = data.(string) + if !ok { + return parameterTypeError("signedKey", "string") + } + } + return nil +} + +func pathSignKey(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "signkey/" + framework.GenericNameRegex("signedKey"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the key that will be used for signing.", + }, + "signedKey": { + Type: framework.TypeString, + Description: "Name of the key that needs to be signed.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathSignKey, + }, + }, + HelpSynopsis: pathSignKeyHelpSyn, + HelpDescription: pathSignKeyHelpDesc, + } +} + +func (b *backend) pathSignKey(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var skp signKeyParameters + err := skp.parseInput(data) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + // Acquire a write lock before modifying the entity. + b.lock.Lock() + defer b.lock.Unlock() + + signerEntity, _, err := b.readEntityFromStorage(ctx, req.Storage, skp.name) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + signedEntity, exportable, err := b.readEntityFromStorage(ctx, req.Storage, skp.signedKey) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + for _, id := range signedEntity.Identities { + err = signedEntity.SignIdentity(id.Name, signerEntity, nil) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + } + + err = b.writeEntityToStorage(ctx, req.Storage, skp.signedKey, signedEntity, exportable) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + return nil, nil +} + +const pathSignKeyHelpSyn = "Sign GPG keys" +const pathSignKeyHelpDesc = ` +This path is used to sign named GPG keys to show trust in the key. +The updated keyring is stored back into vault. +` diff --git a/gpg/path_signkey_test.go b/gpg/path_signkey_test.go new file mode 100644 index 00000000..dd91636c --- /dev/null +++ b/gpg/path_signkey_test.go @@ -0,0 +1,113 @@ +package gpg + +import ( + "context" + "testing" + + "github.com/hashicorp/vault/sdk/logical" +) + +func TestGPG_SignKeyNotExistingSignedKeyReturnsNotFound(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "signkey/test", + } + _, err := b.HandleRequest(context.Background(), req) + + if err == nil { + t.Fatal("Key does not exist but does not return not found") + } +} + +func TestGPG_SignKeyNotExistingSignerKeyReturnsNotFound(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "signkey/test", + Data: map[string]interface{}{ + "name": "signerKey", + }, + } + _, err = b.HandleRequest(context.Background(), req) + + if err == nil { + t.Fatal("Key does not exist but does not return not found") + } +} + +func TestGPG_SignKey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Generate a new signed key. + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/signedKey", + Data: map[string]interface{}{ + "generate": true, + "real_name": "signedKey", + "email": "test@example.com", + "exportable": true, + }, + } + + _, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + // Add the signerKey to the storage. + req = &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/signerKey", + Data: map[string]interface{}{ + "generate": true, + "real_name": "signerKey", + "email": "test2@example.com", + "exportable": true, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req = &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "signkey/signedKey", + Data: map[string]interface{}{ + "name": "signerKey", + }, + } + _, err = b.HandleRequest(context.Background(), req) + + if err != nil { + t.Fatalf("Key signing should have succeeded but it failed: %v", err) + } + + entity, _, err := b.readEntityFromStorage(context.Background(), storage, "signedKey") + if err != nil { + t.Fatalf("Failed to read key from storage after key signing: %v", err) + } + for _, id := range entity.Identities { + if len(id.Signatures) == 0 { + t.Fatalf("Identity %s was not signed", id.Name) + } + } +} diff --git a/gpg/path_subkeys.go b/gpg/path_subkeys.go new file mode 100644 index 00000000..63fe01fd --- /dev/null +++ b/gpg/path_subkeys.go @@ -0,0 +1,280 @@ +package gpg + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" +) + +func parameterTypeError(name, expectedType string) error { + return fmt.Errorf("Type of paramater %s is not %s", name, expectedType) +} + +type subkeyParameters struct { + name string + subkeyID string + canSign bool + canEncrypt bool + keyBits int +} + +func (sp *subkeyParameters) parseInput(input *framework.FieldData) error { + var data interface{} + var ok bool + data, ok = input.GetOk("name") + if ok { + sp.name, ok = data.(string) + if !ok { + return parameterTypeError("name", "string") + } + } + + data, ok = input.GetOk("subkeyID") + if ok { + sp.subkeyID, ok = data.(string) + if !ok { + return parameterTypeError("subkeyID", "string") + } + } + + data = input.Get("key_bits") + sp.keyBits, ok = data.(int) + if !ok { + return parameterTypeError("key_bits", "int") + } + + data = input.Get("canSign") + sp.canSign, ok = data.(bool) + if !ok { + return parameterTypeError("canSign", "bool") + } + + data = input.Get("canEncrypt") + sp.canEncrypt, ok = data.(bool) + if !ok { + return parameterTypeError("canEncrypt", "bool") + } + + return nil +} + +func pathListSubkeys(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "subkeys/" + "(?P.+)?/$", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the key.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: b.pathSubkeyList, + }, + }, + HelpSynopsis: pathPolicyHelpSyn, + HelpDescription: pathPolicyHelpDesc, + } +} + +func pathSubkeys(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "subkeys/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("subkeyID"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the key.", + }, + "subkeyID": { + Type: framework.TypeString, + Description: "Fingerprint of the subkey.", + }, + "canSign": { + Type: framework.TypeBool, + Default: false, + Description: "Allows the subkey to support signing.", + }, + "canEncrypt": { + Type: framework.TypeBool, + Default: true, + Description: "Allows the subkey to support encryption.", + }, + "key_bits": { + Type: framework.TypeInt, + Default: 2048, + Description: "The number of bits to use. Only used if generate is true.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathSubkeyRead, + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathSubkeyCreate, + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: b.pathSubkeyDelete, + }, + }, + HelpSynopsis: pathPolicyHelpSyn, + HelpDescription: pathPolicyHelpDesc, + } +} + +func (b *backend) pathSubkeyRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var sp subkeyParameters + err := sp.parseInput(data) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + // Acquire a read lock before the read operation. + b.lock.RLock() + entity, exportable, err := b.readEntityFromStorage(ctx, req.Storage, sp.name) + b.lock.RUnlock() + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + var subkey *openpgp.Subkey + for _, sk := range entity.Subkeys { + if sp.subkeyID == hex.EncodeToString(sk.PublicKey.Fingerprint[:]) { + subkey = &sk + break + } + } + + if subkey == nil { + err = fmt.Errorf("Subkey with fingerprint %s for key %s was not found", sp.subkeyID, sp.name) + return logical.ErrorResponse(err.Error()), err + } + + var buf bytes.Buffer + w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) + err = serializePublicSubkey(w, entity, subkey) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + w.Close() + + return &logical.Response{ + Data: map[string]interface{}{ + "subkey": buf.String(), + "exportable": exportable, + }, + }, nil +} + +func (b *backend) pathSubkeyCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var sp subkeyParameters + err := sp.parseInput(data) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + // Acquire a write lock before modifying the entity. + b.lock.Lock() + defer b.lock.Unlock() + + entity, exportable, err := b.readEntityFromStorage(ctx, req.Storage, sp.name) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + if sp.keyBits < 2048 { + return logical.ErrorResponse("Keys < 2048 bits are unsafe and not supported"), nil + } + config := packet.Config{ + RSABits: sp.keyBits, + } + + err = entity.AddSubkey(sp.canSign, sp.canEncrypt, &config) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + err = b.writeEntityToStorage(ctx, req.Storage, sp.name, entity, exportable) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "subkey-id": hex.EncodeToString(entity.Subkeys[len(entity.Subkeys)-1].PublicKey.Fingerprint[:]), + "name": sp.name, + }, + }, nil +} + +func (b *backend) pathSubkeyList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var name string + d, ok := data.GetOk("name") + if ok { + name, ok = d.(string) + if !ok { + err := parameterTypeError("name", "string") + return logical.ErrorResponse(err.Error()), err + } + } + + // Acquire a read lock before the read operation. + b.lock.RLock() + entity, _, err := b.readEntityFromStorage(ctx, req.Storage, name) + b.lock.RUnlock() + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + var subkeys []string + for _, sk := range entity.Subkeys { + subkeys = append(subkeys, hex.EncodeToString(sk.PublicKey.Fingerprint[:])) + } + + return logical.ListResponse(subkeys), nil +} + +func (b *backend) pathSubkeyDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var sp subkeyParameters + err := sp.parseInput(data) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + // Acquire a write lock before modifying the entity. + b.lock.Lock() + defer b.lock.Unlock() + + entity, exportable, err := b.readEntityFromStorage(ctx, req.Storage, sp.name) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + var subkeys []openpgp.Subkey + found := false + for _, sk := range entity.Subkeys { + if hex.EncodeToString(sk.PublicKey.Fingerprint[:]) == sp.subkeyID { + found = true + continue + } + subkeys = append(subkeys, sk) + } + if !found { + err = fmt.Errorf("Subkey with fingerprint %s for key %s was not found", sp.subkeyID, sp.name) + return logical.ErrorResponse(err.Error()), err + } + entity.Subkeys = subkeys + + err = b.writeEntityToStorage(ctx, req.Storage, sp.name, entity, exportable) + if err != nil { + return logical.ErrorResponse(err.Error()), err + } + + return nil, nil +} diff --git a/gpg/path_subkeys_test.go b/gpg/path_subkeys_test.go new file mode 100644 index 00000000..764e01f8 --- /dev/null +++ b/gpg/path_subkeys_test.go @@ -0,0 +1,403 @@ +package gpg + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "testing" + + "github.com/hashicorp/vault/sdk/logical" + "golang.org/x/crypto/openpgp" +) + +func addKeyToTestStorage(b *backend, storage logical.Storage, name string, + generate bool, keyString string) error { + if generate { + keyString = "" + } + + // Add the test key to the storage. + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s", name), + Data: map[string]interface{}{ + "generate": generate, + "key": keyString, + }, + } + response, err := b.HandleRequest(context.Background(), req) + if err != nil || response.IsError() { + return fmt.Errorf("failed to create key %s", name) + } + return nil +} + +func TestGPG_SubKeyNotExistingSubkeyReturnsNotFound(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Key does not exist. + req := &logical.Request{ + Storage: storage, + Operation: logical.ListOperation, + Path: "subkeys/test/", + } + _, err := b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("Key does not exist but does not return not found") + } + + // Add the test key to the storage. + err = addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + // Subkey requested does not exist. + req = &logical.Request{ + Storage: storage, + Operation: logical.ReadOperation, + Path: "subkeys/test/keyDoesNotExist", + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("Subkey does not exist but does not return not found") + } + + // Subkey that is being deleted does not exist. + req = &logical.Request{ + Storage: storage, + Operation: logical.DeleteOperation, + Path: "subkeys/test/keyDoesNotExist", + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("Subkey does not exist but does not return not found") + } +} + +func TestGPG_CreateErrorGeneratedSubkeyTooSmallKeyBits(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "subkeys/test", + Data: map[string]interface{}{ + "key_bits": 1024, + }, + } + response, err := b.HandleRequest(context.Background(), req) + + if err != nil { + t.Fatal(err) + } + if !response.IsError() { + t.Fatal("Subkey creation has been accepted but should have denied due to insufficient key size") + } +} + +func TestGPG_CreateErrorGeneratedUnusableSubkey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "subkeys/test", + Data: map[string]interface{}{ + "canSign": false, + "canEncrypt": false, + "key_bits": 1024, + }, + } + response, err := b.HandleRequest(context.Background(), req) + + if err != nil { + t.Fatal(err) + } + if !response.IsError() { + t.Fatal("Subkey creation has been accepted but it cannot be used for signing or encryption") + } +} + +func TestGPG_CreateSubkey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + // Default subkey. + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "subkeys/test", + Data: map[string]interface{}{}, + } + response, err := b.HandleRequest(context.Background(), req) + + if err != nil { + t.Fatal(err) + } + if response.IsError() { + t.Fatal("Failed to create a default subkey") + } + + // Custom subkey. + req = &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "subkeys/test", + Data: map[string]interface{}{ + "canSign": true, + "canEncrypt": true, + }, + } + + response, err = b.HandleRequest(context.Background(), req) + + if err != nil { + t.Fatal(err) + } + if response.IsError() { + t.Fatal("Failed to create a custom subkey") + } +} + +func TestGPG_ListSubkeys(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + req := &logical.Request{ + Storage: storage, + Operation: logical.ListOperation, + Path: "subkeys/test/", + } + response, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if response.IsError() { + t.Fatalf("Failed to list subkeys of an valid key: %v", err) + } + + keys, ok := response.Data["keys"].([]string) + if !ok { + t.Fatal("Subkey IDs not found in list response") + } + + if len(keys) != 1 { + t.Fatal("Subkeys exist but list response for subkeys is empty") + } +} + +func TestGPG_DeleteSubkey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgKey) + if err != nil { + t.Fatal(err) + } + + // Get subkey ID of the subkey. + entity, _, err := b.readEntityFromStorage(context.Background(), storage, "test") + if err != nil { + t.Fatalf("Failed to read key from storage after key revocation: %v", err) + } + subkeyID := hex.EncodeToString(entity.Subkeys[0].PublicKey.Fingerprint[:]) + + req := &logical.Request{ + Storage: storage, + Operation: logical.DeleteOperation, + Path: fmt.Sprintf("subkeys/test/%s", subkeyID), + } + response, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if response.IsError() { + t.Fatalf("Failed to delete subkey of an valid key: %v", err) + } +} + +func TestGPG_ReadSubkey(t *testing.T) { + storage := &logical.InmemStorage{} + b := Backend() + + // Add the test key to the storage. + err := addKeyToTestStorage(b, storage, "test", false, gpgSignedAndRevokedTestKey) + if err != nil { + t.Fatal(err) + } + + entity, _, err := b.readEntityFromStorage(context.Background(), storage, "test") + if err != nil { + t.Fatalf("Failed to read key from storage after key revocation: %v", err) + } + subkeyID := hex.EncodeToString(entity.Subkeys[0].PublicKey.Fingerprint[:]) + req := &logical.Request{ + Storage: storage, + Operation: logical.ReadOperation, + Path: fmt.Sprintf("subkeys/test/%s", subkeyID), + } + response, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if response.IsError() { + t.Fatalf("Subkey exists but failed to read: %v", err) + } + subkey, ok := response.Data["subkey"].(string) + if !ok { + t.Fatalf("no name subkey found in response data %#v", response.Data) + } + r := bytes.NewReader([]byte(subkey)) + keyring, err := openpgp.ReadArmoredKeyRing(r) + if err != nil { + t.Fatal("Unable to import subkey") + } + if len(keyring[0].Revocations) != 1 { + t.Fatal("Missing revocation from the read subkey") + } + if len(keyring[0].Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(keyring[0].Subkeys)) + } +} + +const gpgSignedAndRevokedTestKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +t4mV +=Nbpa +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/gpg/readwrite.go b/gpg/readwrite.go new file mode 100644 index 00000000..52248597 --- /dev/null +++ b/gpg/readwrite.go @@ -0,0 +1,84 @@ +package gpg + +import ( + "bytes" + "context" + "fmt" + + "github.com/hashicorp/vault/sdk/logical" + "golang.org/x/crypto/openpgp" +) + +func (b *backend) readEntityFromStorage(ctx context.Context, storage logical.Storage, name string) (*openpgp.Entity, bool, error) { + entry, err := b.key(ctx, storage, name) + if err != nil { + return nil, false, err + } + if entry == nil { + return nil, false, fmt.Errorf("Key with name %s was not found", name) + } + entity, err := b.entity(entry) + if err != nil { + return nil, false, err + } + return entity, entry.Exportable, nil +} + +func (b *backend) writeEntityToStorage(ctx context.Context, storage logical.Storage, name string, entity *openpgp.Entity, + exportable bool) error { + var buf bytes.Buffer + err := serializeEntityWithAllSignatures(&buf, entity) + if err != nil { + return err + } + + updatedEntry, err := logical.StorageEntryJSON("key/"+name, &keyEntry{ + SerializedKey: buf.Bytes(), + Exportable: exportable, + }) + if err != nil { + return err + } + if err := storage.Put(ctx, updatedEntry); err != nil { + return err + } + + return nil +} + +func (b *backend) readKeyIDToNameMap(ctx context.Context, storage logical.Storage) (map[string]string, error) { + // Acquire a read lock before the read operation. + b.lock.RLock() + entry, err := storage.Get(ctx, "keyIDToNameMap") + b.lock.RUnlock() + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var keyIDToNameMap map[string]string + err = entry.DecodeJSON(&keyIDToNameMap) + if err != nil { + return nil, err + } + return keyIDToNameMap, nil +} + +func (b *backend) writeKeyIDToNameMap(ctx context.Context, storage logical.Storage, + m map[string]string) error { + entry, err := logical.StorageEntryJSON("keyIDToNameMap", m) + if err != nil { + return err + } + + // Acquire a write lock before writing the map. + b.lock.Lock() + defer b.lock.Unlock() + if err := storage.Put(ctx, entry); err != nil { + return err + } + + return nil +} diff --git a/gpg/serialize.go b/gpg/serialize.go new file mode 100644 index 00000000..363f615d --- /dev/null +++ b/gpg/serialize.go @@ -0,0 +1,160 @@ +package gpg + +import ( + "fmt" + "io" + + "golang.org/x/crypto/openpgp" +) + +// openpgp.Serialize does not serialize the revocation signature packets. This method +// implements exporting a public key along with it's corresponding revocations. +func serializeWithRevocations(w io.Writer, e *openpgp.Entity) (err error) { + err = e.PrimaryKey.Serialize(w) + if err != nil { + return err + } + for _, rev := range e.Revocations { + err = rev.Serialize(w) + if err != nil { + return err + } + } + for _, ident := range e.Identities { + err = ident.UserId.Serialize(w) + if err != nil { + return err + } + err = ident.SelfSignature.Serialize(w) + if err != nil { + return err + } + for _, sig := range ident.Signatures { + err = sig.Serialize(w) + if err != nil { + return err + } + } + } + for _, subkey := range e.Subkeys { + err = subkey.PublicKey.Serialize(w) + if err != nil { + return err + } + err = subkey.Sig.Serialize(w) + if err != nil { + return err + } + } + return nil +} + +// openpgp.SerializePrivate resigns the subkeys and drops the signature on identity +// done by other users which shows trust in a key. This method implements a clean export of +// the key preserving the aformentioned signatures. +func serializeEntityWithAllSignatures(w io.Writer, e *openpgp.Entity) error { + var err error + + if e.PrivateKey != nil { + err = e.PrivateKey.Serialize(w) + if err != nil { + return err + } + } else { + return fmt.Errorf("No private key has been found") + } + + for _, r := range e.Revocations { + err = r.Serialize(w) + if err != nil { + return err + } + } + + for _, ident := range e.Identities { + err = ident.UserId.Serialize(w) + if err != nil { + return err + } + err = ident.SelfSignature.Serialize(w) + if err != nil { + return err + } + for _, sig := range ident.Signatures { + err = sig.Serialize(w) + if err != nil { + return err + } + } + } + for _, subkey := range e.Subkeys { + if subkey.PrivateKey != nil { + err = subkey.PrivateKey.Serialize(w) + if err != nil { + return err + } + } + + err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, nil) + if err != nil { + return err + } + // Re-sign the embedded signature as well if it exists. + if subkey.Sig.EmbeddedSignature != nil { + err = subkey.Sig.EmbeddedSignature.CrossSignKey(subkey.PublicKey, e.PrimaryKey, + subkey.PrivateKey, nil) + if err != nil { + return err + } + } + err = subkey.Sig.Serialize(w) + if err != nil { + return err + } + } + + return nil +} + +// serializePublicSubkey serializes the public primary key, associated revocations and identities +// with signatures along with the specified subkey. This method will be used to export a single +// subkey associated with a primary key. +func serializePublicSubkey(w io.Writer, e *openpgp.Entity, subkey *openpgp.Subkey) error { + err := e.PrimaryKey.Serialize(w) + if err != nil { + return err + } + for _, rev := range e.Revocations { + err = rev.Serialize(w) + if err != nil { + return err + } + } + for _, ident := range e.Identities { + err = ident.UserId.Serialize(w) + if err != nil { + return err + } + err = ident.SelfSignature.Serialize(w) + if err != nil { + return err + } + for _, sig := range ident.Signatures { + err = sig.Serialize(w) + if err != nil { + return err + } + } + } + if subkey.PublicKey != nil { + err = subkey.PublicKey.Serialize(w) + if err != nil { + return err + } + } + err = subkey.Sig.Serialize(w) + if err != nil { + return err + } + return nil +}