Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `kinesis` | [Kinesis](./aws/kinesis.md) | - | - | - | 39 |
| `kinesisvideo` | [KinesisVideo](./aws/kinesisvideo.md) | - | - | - | 17 |
| `kms` | [KMS](./aws/kms.md) | - | - | - | 46 |
| `kms-gcp` | - | - | [KMS](./gcp/kms.md) | - | 17 |
| `kms-gcp` | - | - | [KMS](./gcp/kms.md) | - | 22 |
| `kusto` | - | [Kusto](./azure/kusto.md) | - | - | 14 |
| `loadbalancer` | [ELB](./aws/elb.md) | [LB](./azure/lb.md) | [LB](./gcp/lb.md) | - | 19 |
| `loadtesting` | - | [LoadTesting](./azure/loadtesting.md) | - | - | 8 |
Expand Down
25 changes: 20 additions & 5 deletions docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -9827,6 +9827,12 @@
"service": "kms-gcp",
"interface": "provider-native",
"operations": [
{
"name": "AsymmetricDecrypt"
},
{
"name": "AsymmetricSign"
},
{
"name": "CreateCryptoKey"
},
Expand All @@ -9836,20 +9842,29 @@
{
"name": "CreateKeyRing"
},
{
"name": "Decrypt"
},
{
"name": "DestroyCryptoKeyVersion"
},
{
"name": "Encrypt"
},
{
"name": "GenerateRandomBytes"
},
{
"name": "GetCryptoKey"
},
{
"name": "GetCryptoKeyVersion"
},
{
"name": "GetIamPolicy"
"name": "GetKeyRing"
},
{
"name": "GetKeyRing"
"name": "GetPublicKey"
},
{
"name": "ListCryptoKeyVersions"
Expand All @@ -9861,13 +9876,13 @@
"name": "ListKeyRings"
},
{
"name": "RestoreCryptoKeyVersion"
"name": "MacSign"
},
{
"name": "SetIamPolicy"
"name": "MacVerify"
},
{
"name": "TestIamPermissions"
"name": "RestoreCryptoKeyVersion"
},
{
"name": "UpdateCryptoKey"
Expand Down
2 changes: 1 addition & 1 deletion docs/coverage/gcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Services cloudemu emulates for GCP, by native name. Back to the [cross-provider
| [GKEBackup](./gkebackup.md) | `gkebackup` | 11 |
| [GKEHub](./gkehub.md) | `gkehub` | 16 |
| [IAM](./iam.md) | `iam` | 40 |
| [KMS](./kms.md) | (provider-native) | 17 |
| [KMS](./kms.md) | (provider-native) | 22 |
| [LB](./lb.md) | `loadbalancer` | 19 |
| [LRO](./lro.md) | (provider-native) | 1 |
| [ManagedKafka](./managedkafka.md) | `managedkafka` | 11 |
Expand Down
13 changes: 9 additions & 4 deletions docs/coverage/gcp/kms.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@

provider-native `kms-gcp` wire service (GCP-only) · no portable driver · [GCP index](./README.md)

## Operations (17)
## Operations (22)

| Operation | Description |
| --- | --- |
| `AsymmetricDecrypt` | |
| `AsymmetricSign` | |
| `CreateCryptoKey` | |
| `CreateCryptoKeyVersion` | |
| `CreateKeyRing` | |
| `Decrypt` | |
| `DestroyCryptoKeyVersion` | |
| `Encrypt` | |
| `GenerateRandomBytes` | |
| `GetCryptoKey` | |
| `GetCryptoKeyVersion` | |
| `GetIamPolicy` | |
| `GetKeyRing` | |
| `GetPublicKey` | |
| `ListCryptoKeyVersions` | |
| `ListCryptoKeys` | |
| `ListKeyRings` | |
| `MacSign` | |
| `MacVerify` | |
| `RestoreCryptoKeyVersion` | |
| `SetIamPolicy` | |
| `TestIamPermissions` | |
| `UpdateCryptoKey` | |
| `UpdateCryptoKeyPrimaryVersion` | |
| `UpdateCryptoKeyVersion` | |
Expand Down
8 changes: 0 additions & 8 deletions internal/coveragegen/wireops.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,6 @@ var nativeWireOperations = map[string][]string{ //nolint:gochecknoglobals // gen
"gcp/resourcemanager": {
"GetIamPolicy", "SetIamPolicy", "TestIamPermissions",
},
"gcp/kms": {
"CreateCryptoKey", "CreateCryptoKeyVersion", "CreateKeyRing",
"DestroyCryptoKeyVersion", "GetCryptoKey", "GetCryptoKeyVersion",
"GetIamPolicy", "GetKeyRing", "ListCryptoKeyVersions", "ListCryptoKeys",
"ListKeyRings", "RestoreCryptoKeyVersion", "SetIamPolicy",
"TestIamPermissions", "UpdateCryptoKey", "UpdateCryptoKeyPrimaryVersion",
"UpdateCryptoKeyVersion",
},
"gcp/cloudbilling": {
"CreateBillingAccount", "CreateBudget", "DeleteBudget", "GetBillingAccount",
"GetBudget", "GetProjectBillingInfo", "ListBillingAccounts", "ListBudgets",
Expand Down
103 changes: 103 additions & 0 deletions persist/kms_persist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package persist_test

import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"net/http"
"testing"

cloudemu "github.com/stackshy/cloudemu/v2"
gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)

func kmsCall(t *testing.T, h http.Handler, c wireCall) map[string]any {
t.Helper()

code, body := doWire(t, h, c)
if code != http.StatusOK {
t.Fatalf("%s %s = %d %s", c.method, c.path, code, body)
}

var out map[string]any
if err := json.Unmarshal([]byte(body), &out); err != nil {
t.Fatalf("decode %s: %v", body, err)
}

return out
}

// TestKMSKeysSurviveRestore covers GKMS-N1: key rings, crypto keys, versions
// and their key material used to live only in the wire handler, so a
// serve --persist restart lost them and stored ciphertexts became
// undecryptable.
func TestKMSKeysSurviveRestore(t *testing.T) {
const (
loc = "/v1/projects/p1/locations/us-central1"
ring = loc + "/keyRings/r1"
sym = ring + "/cryptoKeys/sym"
sig = ring + "/cryptoKeys/sig/cryptoKeyVersions/1"
)

src := cloudemu.NewGCP()
srcSrv := gcpserver.NewFromProvider(src)

mustWire(t, srcSrv, []wireCall{
{http.MethodPost, loc + "/keyRings?keyRingId=r1", `{}`},
{http.MethodPost, ring + "/cryptoKeys?cryptoKeyId=sym", `{"purpose":"ENCRYPT_DECRYPT"}`},
{http.MethodPost, ring + "/cryptoKeys?cryptoKeyId=sig",
`{"purpose":"ASYMMETRIC_SIGN","versionTemplate":{"algorithm":"EC_SIGN_P256_SHA256"}}`},
{http.MethodPost, ring + ":setIamPolicy",
`{"policy":{"bindings":[{"role":"roles/cloudkms.admin","members":["user:a@example.com"]}]}}`},
})

aad := base64.StdEncoding.EncodeToString([]byte("ctx"))
enc := kmsCall(t, srcSrv, wireCall{http.MethodPost, sym + ":encrypt",
`{"plaintext":"` + base64.StdEncoding.EncodeToString([]byte("hello")) + `","additionalAuthenticatedData":"` + aad + `"}`})

digest := sha256.Sum256([]byte("msg"))
signed := kmsCall(t, srcSrv, wireCall{http.MethodPost, sig + ":asymmetricSign",
`{"digest":{"sha256":"` + base64.StdEncoding.EncodeToString(digest[:]) + `"}}`})
pubBefore := kmsCall(t, srcSrv, wireCall{http.MethodGet, sig + "/publicKey", ""})

dst := cloudemu.NewGCP()
roundTrip(t, "gcp", src.SnapshotServices(), dst.SnapshotServices())
dstSrv := gcpserver.NewFromProvider(dst)

assertSameReads(t, srcSrv, dstSrv, []string{ring, sym, loc + "/keyRings", sym + "/cryptoKeyVersions", ring + ":getIamPolicy"})

dec := kmsCall(t, dstSrv, wireCall{http.MethodPost, sym + ":decrypt",
`{"ciphertext":"` + enc["ciphertext"].(string) + `","additionalAuthenticatedData":"` + aad + `"}`})
if got, _ := base64.StdEncoding.DecodeString(dec["plaintext"].(string)); string(got) != "hello" {
t.Fatalf("decrypt after restore = %q, want hello", got)
}

pubAfter := kmsCall(t, dstSrv, wireCall{http.MethodGet, sig + "/publicKey", ""})
if pubAfter["pem"] != pubBefore["pem"] {
t.Fatalf("public key changed across restore")
}

block, _ := pem.Decode([]byte(pubAfter["pem"].(string)))

pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
t.Fatalf("ParsePKIXPublicKey: %v", err)
}

sigBytes, _ := base64.StdEncoding.DecodeString(signed["signature"].(string))
if !ecdsa.VerifyASN1(pub.(*ecdsa.PublicKey), digest[:], sigBytes) {
t.Fatal("signature made before restore does not verify with the restored public key")
}

// A key restored from a snapshot keeps signing with the same private key.
again := kmsCall(t, dstSrv, wireCall{http.MethodPost, sig + ":asymmetricSign",
`{"digest":{"sha256":"` + base64.StdEncoding.EncodeToString(digest[:]) + `"}}`})

sigAgain, _ := base64.StdEncoding.DecodeString(again["signature"].(string))
if !ecdsa.VerifyASN1(pub.(*ecdsa.PublicKey), digest[:], sigAgain) {
t.Fatal("signature made after restore does not verify with the original public key")
}
}
3 changes: 3 additions & 0 deletions providers/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
gkebackupprov "github.com/stackshy/cloudemu/v2/providers/gcp/gkebackup"
gkehubprov "github.com/stackshy/cloudemu/v2/providers/gcp/gkehub"
"github.com/stackshy/cloudemu/v2/providers/gcp/iam"
kmsprov "github.com/stackshy/cloudemu/v2/providers/gcp/kms"
"github.com/stackshy/cloudemu/v2/providers/gcp/loadbalancer"
managedkafkaprov "github.com/stackshy/cloudemu/v2/providers/gcp/managedkafka"
"github.com/stackshy/cloudemu/v2/providers/gcp/memorystore"
Expand Down Expand Up @@ -119,6 +120,7 @@ type Provider struct {
CertificateManager *certmanagerprov.Mock
AccessContextManager *acmprov.Mock
PrivateCA *privatecaprov.Mock
KMS *kmsprov.Mock
Dataplex *dataplexprov.Mock
Metastore *metastoreprov.Mock
VPCAccess *vpcaccessprov.Mock
Expand Down Expand Up @@ -194,6 +196,7 @@ func New(opts ...config.Option) *Provider {
CertificateManager: certmanagerprov.New(o),
AccessContextManager: acmprov.New(o),
PrivateCA: privatecaprov.New(o),
KMS: kmsprov.New(o),
Dataplex: dataplexprov.New(o),
Metastore: metastoreprov.New(o),
VPCAccess: vpcaccessprov.New(o),
Expand Down
Loading
Loading