// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package http import ( "context" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "net/http" "strconv" "strings" "testing" "github.com/go-test/deep" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" "github.com/hashicorp/vault/vault/seal" "github.com/hashicorp/vault/version" ) func TestSysSealStatus(t *testing.T) { core := vault.TestCore(t) vault.TestCoreInit(t, core) ln, addr := TestServer(t, core) defer ln.Close() resp, err := http.Get(addr + "/v1/sys/seal-status") if err != nil { t.Fatalf("err: %s", err) } var actual map[string]interface{} expected := map[string]interface{}{ "sealed": true, "t": json.Number("3"), "n": json.Number("3"), "progress": json.Number("0"), "nonce": "", "type": "shamir", "recovery_seal": false, "initialized": true, "migration": false, "build_date": version.BuildDate, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) if actual["version"] == nil { t.Fatalf("expected version information") } expected["version"] = actual["version"] if actual["cluster_name"] == nil { delete(expected, "cluster_name") } else { expected["cluster_name"] = actual["cluster_name"] } if actual["cluster_id"] == nil { delete(expected, "cluster_id") } else { expected["cluster_id"] = actual["cluster_id"] } if diff := deep.Equal(actual, expected); diff != nil { t.Fatal(diff) } } func TestSysSealStatus_uninit(t *testing.T) { core := vault.TestCore(t) ln, addr := TestServer(t, core) defer ln.Close() resp, err := http.Get(addr + "/v1/sys/seal-status") if err != nil { t.Fatalf("err: %s", err) } testResponseStatus(t, resp, 200) } func TestSysSeal(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := TestServer(t, core) defer ln.Close() TestServerAuth(t, addr, token) resp := testHttpPut(t, token, addr+"/v1/sys/seal", nil) testResponseStatus(t, resp, 204) if !core.Sealed() { t.Fatal("should be sealed") } } func TestSysSeal_unsealed(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := TestServer(t, core) defer ln.Close() TestServerAuth(t, addr, token) resp := testHttpPut(t, token, addr+"/v1/sys/seal", nil) testResponseStatus(t, resp, 204) if !core.Sealed() { t.Fatal("should be sealed") } } func TestSysUnseal(t *testing.T) { core := vault.TestCore(t) keys, _ := vault.TestCoreInit(t, core) ln, addr := TestServer(t, core) defer ln.Close() for i, key := range keys { resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ "key": hex.EncodeToString(key), }) var actual map[string]interface{} expected := map[string]interface{}{ "sealed": true, "t": json.Number("3"), "n": json.Number("3"), "progress": json.Number(fmt.Sprintf("%d", i+1)), "nonce": "", "type": "shamir", "recovery_seal": false, "initialized": true, "migration": false, "build_date": version.BuildDate, } if i == len(keys)-1 { expected["sealed"] = false expected["progress"] = json.Number("0") } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) if i < len(keys)-1 && (actual["nonce"] == nil || actual["nonce"].(string) == "") { t.Fatalf("got nil nonce, actual is %#v", actual) } else { expected["nonce"] = actual["nonce"] } if actual["version"] == nil { t.Fatalf("expected version information") } expected["version"] = actual["version"] if actual["cluster_name"] == nil { delete(expected, "cluster_name") } else { expected["cluster_name"] = actual["cluster_name"] } if actual["cluster_id"] == nil { delete(expected, "cluster_id") } else { expected["cluster_id"] = actual["cluster_id"] } if diff := deep.Equal(actual, expected); diff != nil { t.Fatal(diff) } } } func subtestBadSingleKey(t *testing.T, seal vault.Seal) { core := vault.TestCoreWithSeal(t, seal, false) _, err := core.Initialize(context.Background(), &vault.InitParams{ BarrierConfig: &vault.SealConfig{ SecretShares: 1, SecretThreshold: 1, }, RecoveryConfig: &vault.SealConfig{ SecretShares: 1, SecretThreshold: 1, }, }) if err != nil { t.Fatalf("err: %s", err) } ln, addr := TestServer(t, core) defer ln.Close() testCases := []struct { description string key string }{ // hex key tests // hexadecimal strings have 2 symbols per byte; size(0xAA) == 1 byte { "short hex key", strings.Repeat("AA", 8), }, { "long hex key", strings.Repeat("AA", 34), }, { "uneven hex key byte length", strings.Repeat("AA", 33), }, { "valid hex key but wrong cluster", "4482691dd3a710723c4f77c4920ee21b96c226bf4829fa6eb8e8262c180ae933", }, // base64 key tests // base64 strings have min. 1 character per byte; size("m") == 1 byte { "short b64 key", base64.StdEncoding.EncodeToString([]byte(strings.Repeat("m", 8))), }, { "long b64 key", base64.StdEncoding.EncodeToString([]byte(strings.Repeat("m", 34))), }, { "uneven b64 key byte length", base64.StdEncoding.EncodeToString([]byte(strings.Repeat("m", 33))), }, { "valid b64 key but wrong cluster", "RIJpHdOnEHI8T3fEkg7iG5bCJr9IKfpuuOgmLBgK6TM=", }, // other key tests { "empty key", "", }, { "key with bad format", "ThisKeyIsNeitherB64NorHex", }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ "key": tc.key, }) testResponseStatus(t, resp, 400) }) } } func subtestBadMultiKey(t *testing.T, seal vault.Seal) { numKeys := 3 core := vault.TestCoreWithSeal(t, seal, false) _, err := core.Initialize(context.Background(), &vault.InitParams{ BarrierConfig: &vault.SealConfig{ SecretShares: numKeys, SecretThreshold: numKeys, }, RecoveryConfig: &vault.SealConfig{ SecretShares: numKeys, SecretThreshold: numKeys, }, }) if err != nil { t.Fatalf("err: %s", err) } ln, addr := TestServer(t, core) defer ln.Close() testCases := []struct { description string keys []string }{ { "all unseal keys from another cluster", []string{ "b189d98fdec3a15bed9b1cce5088f82b92896696b788c07bdf03c73da08279a5e8", "0fa98232f034177d8d9c2824899a2ac1e55dc6799348533e10510b856aef99f61a", "5344f5caa852f9ba1967d9623ed286a45ea7c4a529522d25f05d29ff44f17930ac", }, }, { "mixing unseal keys from different cluster, different share config", []string{ "b189d98fdec3a15bed9b1cce5088f82b92896696b788c07bdf03c73da08279a5e8", "0fa98232f034177d8d9c2824899a2ac1e55dc6799348533e10510b856aef99f61a", "e04ea3020838c2050c4a169d7ba4d30e034eec8e83e8bed9461bf2646ee412c0", }, }, { "mixing unseal keys from different clusters, similar share config", []string{ "b189d98fdec3a15bed9b1cce5088f82b92896696b788c07bdf03c73da08279a5e8", "0fa98232f034177d8d9c2824899a2ac1e55dc6799348533e10510b856aef99f61a", "413f80521b393aa6c4e42e9a3a3ab7f00c2002b2c3bf1e273fc6f363f35f2a378b", }, }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { for i, key := range tc.keys { resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ "key": key, }) if i == numKeys-1 { // last key testResponseStatus(t, resp, 400) } else { // unseal in progress testResponseStatus(t, resp, 200) } } }) } } func TestSysUnseal_BadKeyNewShamir(t *testing.T) { seal := vault.NewTestSeal(t, &seal.TestSealOpts{StoredKeys: seal.StoredKeysSupportedShamirRoot}) subtestBadSingleKey(t, seal) subtestBadMultiKey(t, seal) } func TestSysUnseal_BadKeyAutoUnseal(t *testing.T) { seal := vault.NewTestSeal(t, &seal.TestSealOpts{StoredKeys: seal.StoredKeysSupportedGeneric}) subtestBadSingleKey(t, seal) subtestBadMultiKey(t, seal) } func TestSysUnseal_Reset(t *testing.T) { core := vault.TestCore(t) ln, addr := TestServer(t, core) defer ln.Close() thresh := 3 resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ "secret_shares": 5, "secret_threshold": thresh, }) var actual map[string]interface{} testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) keysRaw, ok := actual["keys"] if !ok { t.Fatalf("no keys: %#v", actual) } for i, key := range keysRaw.([]interface{}) { if i > thresh-2 { break } resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ "key": key.(string), }) var actual map[string]interface{} expected := map[string]interface{}{ "sealed": true, "t": json.Number("3"), "n": json.Number("5"), "progress": json.Number(strconv.Itoa(i + 1)), "type": "shamir", "recovery_seal": false, "initialized": true, "migration": false, "build_date": version.BuildDate, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) if actual["version"] == nil { t.Fatalf("expected version information") } expected["version"] = actual["version"] if actual["nonce"] == "" && expected["sealed"].(bool) { t.Fatalf("expected a nonce") } expected["nonce"] = actual["nonce"] if actual["cluster_name"] == nil { delete(expected, "cluster_name") } else { expected["cluster_name"] = actual["cluster_name"] } if actual["cluster_id"] == nil { delete(expected, "cluster_id") } else { expected["cluster_id"] = actual["cluster_id"] } if diff := deep.Equal(actual, expected); diff != nil { t.Fatal(diff) } } resp = testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ "reset": true, }) actual = map[string]interface{}{} expected := map[string]interface{}{ "sealed": true, "t": json.Number("3"), "n": json.Number("5"), "progress": json.Number("0"), "type": "shamir", "recovery_seal": false, "initialized": true, "build_date": version.BuildDate, "migration": false, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) if actual["version"] == nil { t.Fatalf("expected version information") } expected["version"] = actual["version"] expected["nonce"] = actual["nonce"] if actual["cluster_name"] == nil { delete(expected, "cluster_name") } else { expected["cluster_name"] = actual["cluster_name"] } if actual["cluster_id"] == nil { delete(expected, "cluster_id") } else { expected["cluster_id"] = actual["cluster_id"] } if diff := deep.Equal(actual, expected); diff != nil { t.Fatal(diff) } } // Test Seal's permissions logic, which is slightly different than normal code // paths in that it queries the ACL rather than having checkToken do it. This // is because it was abusing RootPaths in logical_system, but that caused some // haywire with code paths that expected there to be an actual corresponding // logical.Path for it. This way is less hacky, but this test ensures that we // have not opened up a permissions hole. func TestSysSeal_Permissions(t *testing.T) { core, _, root := vault.TestCoreUnsealed(t) ln, addr := TestServer(t, core) defer ln.Close() TestServerAuth(t, addr, root) // Set the 'test' policy object to permit write access to sys/seal req := &logical.Request{ Operation: logical.UpdateOperation, Path: "sys/policy/test", Data: map[string]interface{}{ "rules": `path "sys/seal" { capabilities = ["read"] }`, }, ClientToken: root, } resp, err := core.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v", err) } if resp == nil || resp.IsError() { t.Fatalf("bad: %#v", resp) } // Create a non-root token with access to that policy req.Path = "auth/token/create" req.Data = map[string]interface{}{ "id": "child", "policies": []string{"test"}, } resp, err = core.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v %v", err, resp) } if resp.Auth.ClientToken != "child" { t.Fatalf("bad: %#v", resp) } // We must go through the HTTP interface since seal doesn't go through HandleRequest // We expect this to fail since it needs update and sudo httpResp := testHttpPut(t, "child", addr+"/v1/sys/seal", nil) testResponseStatus(t, httpResp, 403) // Now modify to add update capability req = &logical.Request{ Operation: logical.UpdateOperation, Path: "sys/policy/test", Data: map[string]interface{}{ "rules": `path "sys/seal" { capabilities = ["update"] }`, }, ClientToken: root, } resp, err = core.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v", err) } if resp == nil || resp.IsError() { t.Fatalf("bad: %#v", resp) } // We expect this to fail since it needs sudo httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil) testResponseStatus(t, httpResp, 403) // Now modify to just sudo capability req = &logical.Request{ Operation: logical.UpdateOperation, Path: "sys/policy/test", Data: map[string]interface{}{ "rules": `path "sys/seal" { capabilities = ["sudo"] }`, }, ClientToken: root, } resp, err = core.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v", err) } if resp == nil || resp.IsError() { t.Fatalf("bad: %#v", resp) } // We expect this to fail since it needs update httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil) testResponseStatus(t, httpResp, 403) // Now modify to add all needed capabilities req = &logical.Request{ Operation: logical.UpdateOperation, Path: "sys/policy/test", Data: map[string]interface{}{ "rules": `path "sys/seal" { capabilities = ["update", "sudo"] }`, }, ClientToken: root, } resp, err = core.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v", err) } if resp == nil || resp.IsError() { t.Fatalf("bad: %#v", resp) } // We expect this to work httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil) testResponseStatus(t, httpResp, 204) } func TestSysStepDown(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := TestServer(t, core) defer ln.Close() TestServerAuth(t, addr, token) resp := testHttpPut(t, token, addr+"/v1/sys/step-down", nil) testResponseStatus(t, resp, 204) }