Skip to content

Commit

Permalink
Add path for retrieving the public key (#5)
Browse files Browse the repository at this point in the history
* Add path for retrieving the public key

* Use --get for curl
  • Loading branch information
sethvargo authored and briankassouf committed Dec 12, 2018
1 parent a7d4151 commit 6cd9918
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 1 deletion.
1 change: 1 addition & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func Backend() *backend {

b.pathDecrypt(),
b.pathEncrypt(),
b.pathPubkey(),
b.pathReencrypt(),
b.pathSign(),
b.pathVerify(),
Expand Down
97 changes: 97 additions & 0 deletions path_pubkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package gcpkms

import (
"context"
"fmt"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"

kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
)

func (b *backend) pathPubkey() *framework.Path {
return &framework.Path{
Pattern: "pubkey/" + framework.GenericNameRegex("key"),

HelpSynopsis: "Retrieve the public key associated with the named key",
HelpDescription: `
Retrieve the PEM-encoded Google Cloud KMS public key associated with the Vault
named key. The key will only be available if the key is asymmetric.
`,

Fields: map[string]*framework.FieldSchema{
"key": &framework.FieldSchema{
Type: framework.TypeString,
Description: `
Name of the key for which to get the public key. This key must already exist in
Vault and Google Cloud KMS.
`,
},

"key_version": &framework.FieldSchema{
Type: framework.TypeInt,
Description: `
Integer version of the crypto key version from which to exact the public key.
This field is required.
`,
},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: withFieldValidator(b.pathPubkeyRead),
},
}
}

// pathPubkeyRead corresponds to GET gcpkms/pubkey/:key and is used to read the
// public key contents of the crypto key version.
func (b *backend) pathPubkeyRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
key := d.Get("key").(string)
keyVersion := d.Get("key_version").(int)

if keyVersion == 0 {
return nil, errMissingFields("key_version")
}

k, err := b.Key(ctx, req.Storage, key)
if err != nil {
if err == ErrKeyNotFound {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, err
}

if k.MinVersion > 0 && keyVersion < k.MinVersion {
resp := fmt.Sprintf("requested version %d is less than minimum allowed version of %d",
keyVersion, k.MinVersion)
return logical.ErrorResponse(resp), logical.ErrPermissionDenied
}

if k.MaxVersion > 0 && keyVersion > k.MaxVersion {
resp := fmt.Sprintf("requested version %d is greater than maximum allowed version of %d",
keyVersion, k.MaxVersion)
return logical.ErrorResponse(resp), logical.ErrPermissionDenied
}

kmsClient, closer, err := b.KMSClient(req.Storage)
if err != nil {
return nil, err
}
defer closer()

pk, err := kmsClient.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{
Name: fmt.Sprintf("%s/cryptoKeyVersions/%d", k.CryptoKeyID, keyVersion),
})
if err != nil {
return nil, errwrap.Wrapf("failed to get public key: {{err}}", err)
}

return &logical.Response{
Data: map[string]interface{}{
"pem": pk.Pem,
"algorithm": algorithmToString(pk.Algorithm),
},
}, nil
}
153 changes: 153 additions & 0 deletions path_pubkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package gcpkms

import (
"context"
"crypto/x509"
"encoding/pem"
"strings"
"testing"

"github.com/hashicorp/vault/logical"

kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
)

func TestPathPubkey_Read(t *testing.T) {
t.Parallel()

t.Run("field_validation", func(t *testing.T) {
t.Parallel()
testFieldValidation(t, logical.ReadOperation, "pubkey/my-key")
})

t.Run("asymmetric_decrypt", func(t *testing.T) {
t.Parallel()

algorithms := []kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA256,
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA256,
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA256,
}

for _, algo := range algorithms {
algo := algo
name := strings.ToLower(algo.String())

t.Run(name, func(t *testing.T) {
t.Parallel()

cryptoKey, cleanup := testCreateKMSCryptoKeyAsymmetricDecrypt(t, algo)
defer cleanup()

b, storage := testBackend(t)

ctx := context.Background()
if err := storage.Put(ctx, &logical.StorageEntry{
Key: "keys/my-key",
Value: []byte(`{"name":"my-key", "crypto_key_id":"` + cryptoKey + `"}`),
}); err != nil {
t.Fatal(err)
}

// Get the public key
resp, err := b.HandleRequest(ctx, &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "pubkey/my-key",
Data: map[string]interface{}{
"key_version": 1,
},
})
if err != nil {
t.Fatal(err)
}

// Verify it's a pem public key (this is kinda testing KMS, but it's
// a good test to ensure the API doesn't change).
pemStr, ok := resp.Data["pem"].(string)
if !ok {
t.Fatal("missing pem")
}

// Extract the PEM-encoded data block
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
t.Fatalf("not pem: %s", pemStr)
}

// Decode the public key
if _, err := x509.ParsePKIXPublicKey(block.Bytes); err != nil {
t.Fatal(err)
}
})
}
})

t.Run("asymmetric_sign", func(t *testing.T) {
t.Parallel()

algorithms := []kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256,
kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256,
kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384,
}

for _, algo := range algorithms {
algo := algo
name := strings.ToLower(algo.String())

t.Run(name, func(t *testing.T) {
t.Parallel()

cryptoKey, cleanup := testCreateKMSCryptoKeyAsymmetricSign(t, algo)
defer cleanup()

b, storage := testBackend(t)

ctx := context.Background()
if err := storage.Put(ctx, &logical.StorageEntry{
Key: "keys/my-key",
Value: []byte(`{"name":"my-key", "crypto_key_id":"` + cryptoKey + `"}`),
}); err != nil {
t.Fatal(err)
}

// Get the public key
resp, err := b.HandleRequest(ctx, &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "pubkey/my-key",
Data: map[string]interface{}{
"key_version": 1,
},
})
if err != nil {
t.Fatal(err)
}

// Verify it's a pem public key (this is kinda testing KMS, but it's
// a good test to ensure the API doesn't change).
pemStr, ok := resp.Data["pem"].(string)
if !ok {
t.Fatal("missing pem")
}

// Extract the PEM-encoded data block
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
t.Fatalf("not pem: %s", pemStr)
}

// Decode the public key
if _, err := x509.ParsePKIXPublicKey(block.Bytes); err != nil {
t.Fatal(err)
}
})
}
})
}
59 changes: 59 additions & 0 deletions website/source/api/secret/gcpkms/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,65 @@ $ curl \
}
```

## Get Public Key

This endpoint uses the named encryption key to retrieve the PEM-encoded contents
of the corresponding public key on Google Cloud KMS. This only applies to
asymmetric key types.

| Method | Path | Produces |
| :------- | :--------------------------| :------------------------ |
| `GET` | `gcpkms/pubkey/:key` | `200 application/json` |

### Example Policy

```hcl
path "gcpkms/pubkey/my-key" {
capabilities = ["read"]
}
```

### Parameters

- `key` (`string: <required>`) -
Name of the key in Vault for which to retrieve the public key. This key must
already exist in Vault and must map back to a Google Cloud KMS key. This is
specified as part of the URL.

- `key_version` (`int: <required>`) -
Integer version of the crypto key version for which to retrieve the public key.
This field is required.


### Sample Payload

```json
{
"key_version": 1
}
```

### Sample Request

```text
$ curl \
--header "X-Vault-Token: ..." \
--get \
--data @payload.json \
https://127.0.0.1:8200/v1/gcpkms/pubkey/my-key
```

### Sample Response

```json
{
"data": {
"pem": "----BEGIN PUBLIC KEY-----\nMIICIjANBgkq...",
"algorithm": "rsa_decrypt_oaep_4096_sha256"
}
}
```

## Re-Encrypt Existing Ciphertext

This endpoint uses the named encryption key to re-encrypt the underlying
Expand Down
10 changes: 9 additions & 1 deletion website/source/docs/secrets/gcpkms/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,15 @@ decrypts the value using the corresponding public key.
algorithm=rsa_decrypt_oaep_4096_sha256
```
1. Retrieve the public key from Cloud KMS:
1. Retrieve the public key:
You can use Vault's `pubkey/:key` endpoint:
```text
$ vault read -field=pem gcpkms/pubkey/my-key key_version=1 > ~/mykey.pub
```
Or you can retrieve the values using `gcloud` or the Google Cloud API:
```text
$ gcloud alpha kms keys versions get-public-key [CRYPTO_KEY_VERSION] \
Expand Down

0 comments on commit 6cd9918

Please sign in to comment.