From 6f36f45024974e155da348e21c8fa2bd3dcd17f0 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Tue, 2 Aug 2022 15:52:09 +0200 Subject: [PATCH] kestest: move server to gateway tests This commit refactors the KES server tests. Since the KES stateful and stateless server get separate APIs, this commit refactors the existing server tests and re-implements them as gateway tests. A gateway is truly stateless w.r.t. identities and policies. Signed-off-by: Andreas Auernhammer --- kestest/example_test.go | 8 +- kestest/gateway.go | 217 ++++++++++++++++++ kestest/{server_test.go => gateway_test.go} | 240 ++++++++------------ kestest/policy.go | 6 +- kestest/server.go | 217 ------------------ 5 files changed, 316 insertions(+), 372 deletions(-) create mode 100644 kestest/gateway.go rename kestest/{server_test.go => gateway_test.go} (76%) diff --git a/kestest/example_test.go b/kestest/example_test.go index e4347710..8d40107d 100644 --- a/kestest/example_test.go +++ b/kestest/example_test.go @@ -14,8 +14,8 @@ import ( "github.com/minio/kes/kestest" ) -func ExampleServer() { - server := kestest.NewServer() +func ExampleGateway() { + server := kestest.NewGateway() defer server.Close() version, err := server.Client().Version(context.Background()) @@ -28,8 +28,8 @@ func ExampleServer() { // v0.0.0-dev } -func ExampleServer_IssueClientCertificate() { - server := kestest.NewServer() +func ExampleGateway_IssueClientCertificate() { + server := kestest.NewGateway() defer server.Close() server.Policy().Allow("test-policy", diff --git a/kestest/gateway.go b/kestest/gateway.go new file mode 100644 index 00000000..e96f692d --- /dev/null +++ b/kestest/gateway.go @@ -0,0 +1,217 @@ +// Copyright 2021 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +// Package kestest provides utilities for end-to-end +// KES testing. +package kestest + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "net/http/httptest" + "time" + + "github.com/minio/kes" + "github.com/minio/kes/internal/auth" + xhttp "github.com/minio/kes/internal/http" + "github.com/minio/kes/internal/key" + "github.com/minio/kes/internal/log" + "github.com/minio/kes/internal/mem" + "github.com/minio/kes/internal/metric" +) + +// NewGateway starts and returns a new Gateway. +// The caller should call Close when finished, +// to shut it down. +func NewGateway() *Gateway { + g := &Gateway{} + g.start() + return g +} + +// A Gateway is a KES gateway listening on a system-chosen +// port on the local loopback interface, for use in +// end-to-end tests. +type Gateway struct { + URL string + + policies *PolicySet + client *kes.Client + + caPrivateKey crypto.PrivateKey + caCertificate *x509.Certificate + + server *httptest.Server +} + +// Client returns a KES client configured for making requests +// to the Gateway as admin identity. +// +// It is configured to trust the Gateway's TLS test certificate. +func (g *Gateway) Client() *kes.Client { return g.client } + +// Policy returns the PolicySet that contains all KES policies +// and identity-policy associations. +func (g *Gateway) Policy() *PolicySet { return g.policies } + +// Close shuts down the Gateway and blocks until all outstanding +// requests on this server have completed. +func (g *Gateway) Close() { g.server.Close() } + +// IssueClientCertificate returns a new TLS certificate for +// client authentication with the given common name. +// +// The returned certificate is issued by a testing CA that is +// trusted by the Gateway. +func (g *Gateway) IssueClientCertificate(name string) tls.Certificate { + if g.caCertificate == nil || g.caPrivateKey == nil { + g.caPrivateKey, g.caCertificate = newCA() + } + return issueCertificate(name, g.caCertificate, g.caPrivateKey, x509.ExtKeyUsageClientAuth) +} + +// CAs returns the Gateway's root CAs. +func (g *Gateway) CAs() *x509.CertPool { + if g.caCertificate == nil || g.caPrivateKey == nil { + g.caPrivateKey, g.caCertificate = newCA() + } + + certpool := x509.NewCertPool() + certpool.AddCert(g.caCertificate) + return certpool +} + +func (g *Gateway) start() { + var ( + rootCAs = g.CAs() + auditLog = log.NewTarget(io.Discard) + errorLog = log.NewTarget(io.Discard) + metrics = metric.New() + adminCert = g.IssueClientCertificate("kestest: admin") + ) + g.policies = &PolicySet{ + admin: Identify(&adminCert), + policies: make(map[string]*auth.Policy), + identities: make(map[kes.Identity]auth.IdentityInfo), + } + + errorLog.Add(metrics.ErrorEventCounter()) + auditLog.Add(metrics.AuditEventCounter()) + store := key.NewCache(&mem.Store{}, &key.CacheConfig{ + Expiry: 30 * time.Second, + ExpiryUnused: 5 * time.Second, + }) + + serverCert := issueCertificate("kestest: gateway", g.caCertificate, g.caPrivateKey, x509.ExtKeyUsageServerAuth) + g.server = httptest.NewUnstartedServer(xhttp.NewGatewayMux(&xhttp.GatewayConfig{ + Keys: store, + Policies: g.policies.policySet(), + Identities: g.policies.identitySet(), + Proxy: nil, + AuditLog: auditLog, + ErrorLog: errorLog, + Metrics: metrics, + })) + g.server.TLS = &tls.Config{ + RootCAs: rootCAs, + ClientCAs: rootCAs, + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.RequireAndVerifyClientCert, + } + g.server.StartTLS() + g.URL = g.server.URL + + g.client = kes.NewClientWithConfig(g.URL, &tls.Config{ + Certificates: []tls.Certificate{adminCert}, + RootCAs: rootCAs, + }) +} + +func newCA() (crypto.PrivateKey, *x509.Certificate) { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(fmt.Sprintf("kestest: failed to generate CA private key: %v", err)) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + panic(fmt.Sprintf("kestest: failed to generate CA certificate serial number: %v", err)) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "kestest Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + } + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey) + if err != nil { + panic(fmt.Sprintf("kestest: failed to generate CA certificate: %v", err)) + } + certificate, err := x509.ParseCertificate(certBytes) + if err != nil { + panic(fmt.Sprintf("kestest: failed to generate CA certificate: %v", err)) + } + return privateKey, certificate +} + +func issueCertificate(name string, caCert *x509.Certificate, caKey crypto.PrivateKey, extKeyUsage ...x509.ExtKeyUsage) tls.Certificate { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(fmt.Sprintf("kestest: failed to generate private/public key pair: %v", err)) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + panic(fmt.Sprintf("kestest: failed to generate certificate serial number: %v", err)) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: name, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: extKeyUsage, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + DNSNames: []string{"localhost"}, + BasicConstraintsValid: true, + } + + rawCertificate, err := x509.CreateCertificate(rand.Reader, &template, caCert, publicKey, caKey) + if err != nil { + panic(fmt.Sprintf("kestest: failed to create certificate: %v", err)) + } + rawPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + panic(fmt.Sprintf("kestest: failed to create certificate: %v", err)) + } + certificate, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: rawCertificate, + }), pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: rawPrivateKey, + })) + if err != nil { + panic(fmt.Sprintf("kestest: failed to create certificate: %v", err)) + } + return certificate +} diff --git a/kestest/server_test.go b/kestest/gateway_test.go similarity index 76% rename from kestest/server_test.go rename to kestest/gateway_test.go index 610dc1dd..b6d22488 100644 --- a/kestest/server_test.go +++ b/kestest/gateway_test.go @@ -10,10 +10,10 @@ import ( "context" "crypto/tls" "encoding/base64" + "fmt" "net/http" "sort" "strconv" - "strings" "testing" "time" @@ -21,7 +21,7 @@ import ( "github.com/minio/kes/kestest" ) -var serverAPIs = []kes.API{ +var gatewayAPIs = []kes.API{ {Method: http.MethodGet, Path: "/version", MaxBody: 0, Timeout: 15 * time.Second}, // 0 {Method: http.MethodGet, Path: "/v1/status", MaxBody: 0, Timeout: 15 * time.Second}, // 1 {Method: http.MethodGet, Path: "/v1/metrics", MaxBody: 0, Timeout: 15 * time.Second}, // 2 @@ -36,32 +36,23 @@ var serverAPIs = []kes.API{ {Method: http.MethodPost, Path: "/v1/key/bulk/decrypt/", MaxBody: 1 << 20, Timeout: 15 * time.Second}, // 10 {Method: http.MethodGet, Path: "/v1/key/list/", MaxBody: 0, Timeout: 15 * time.Second}, // 11 - {Method: http.MethodGet, Path: "/v1/policy/describe/", MaxBody: 0, Timeout: 15 * time.Second}, // 12 - {Method: http.MethodPost, Path: "/v1/policy/assign/", MaxBody: 1024, Timeout: 15 * time.Second}, // 13 - {Method: http.MethodGet, Path: "/v1/policy/read/", MaxBody: 0, Timeout: 15 * time.Second}, // 14 - {Method: http.MethodPost, Path: "/v1/policy/write/", MaxBody: 1 << 20, Timeout: 15 * time.Second}, // 15 - {Method: http.MethodGet, Path: "/v1/policy/list/", MaxBody: 0, Timeout: 15 * time.Second}, // 16 - {Method: http.MethodDelete, Path: "/v1/policy/delete/", MaxBody: 0, Timeout: 15 * time.Second}, // 17 + {Method: http.MethodGet, Path: "/v1/policy/describe/", MaxBody: 0, Timeout: 15 * time.Second}, // 12 + {Method: http.MethodGet, Path: "/v1/policy/read/", MaxBody: 0, Timeout: 15 * time.Second}, // 14 + {Method: http.MethodGet, Path: "/v1/policy/list/", MaxBody: 0, Timeout: 15 * time.Second}, // 15 - {Method: http.MethodGet, Path: "/v1/identity/describe/", MaxBody: 0, Timeout: 15 * time.Second}, // 18 - {Method: http.MethodGet, Path: "/v1/identity/self/describe", MaxBody: 0, Timeout: 15 * time.Second}, // 19 - {Method: http.MethodGet, Path: "/v1/identity/list/", MaxBody: 0, Timeout: 15 * time.Second}, // 20 - {Method: http.MethodDelete, Path: "/v1/identity/delete/", MaxBody: 0, Timeout: 15 * time.Second}, // 21 + {Method: http.MethodGet, Path: "/v1/identity/describe/", MaxBody: 0, Timeout: 15 * time.Second}, // 17 + {Method: http.MethodGet, Path: "/v1/identity/self/describe", MaxBody: 0, Timeout: 15 * time.Second}, // 18 + {Method: http.MethodGet, Path: "/v1/identity/list/", MaxBody: 0, Timeout: 15 * time.Second}, // 19 - {Method: http.MethodGet, Path: "/v1/log/error", MaxBody: 0, Timeout: 0}, // 22 - {Method: http.MethodGet, Path: "/v1/log/audit", MaxBody: 0, Timeout: 0}, // 23 - - {Method: http.MethodPost, Path: "/v1/enclave/create/", MaxBody: 1 << 20, Timeout: 15 * time.Second}, // 24 - {Method: http.MethodDelete, Path: "/v1/enclave/delete/", MaxBody: 0, Timeout: 15 * time.Second}, // 25 - - {Method: http.MethodPost, Path: "/v1/sys/seal", MaxBody: 0, Timeout: 15 * time.Second}, // 26 + {Method: http.MethodGet, Path: "/v1/log/error", MaxBody: 0, Timeout: 0}, // 20 + {Method: http.MethodGet, Path: "/v1/log/audit", MaxBody: 0, Timeout: 0}, // 21 } func TestAPIs(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -70,21 +61,21 @@ func TestAPIs(t *testing.T) { if err != nil { t.Fatalf("Failed fetch server APIs: %v", err) } - if len(apis) != len(serverAPIs) { - t.Fatalf("API mismatch: got len '%d' - want len '%d'", len(apis), len(serverAPIs)) + if len(apis) != len(gatewayAPIs) { + t.Fatalf("API mismatch: got len '%d' - want len '%d'", len(apis), len(gatewayAPIs)) } for i := range apis { - if apis[i].Method != serverAPIs[i].Method { - t.Fatalf("API %d: method mismatch: got '%s' - want '%s'", i, apis[i].Method, serverAPIs[i].Method) + if apis[i].Method != gatewayAPIs[i].Method { + t.Fatalf("API %d: method mismatch: got '%s' - want '%s'", i, apis[i].Method, gatewayAPIs[i].Method) } - if apis[i].Path != serverAPIs[i].Path { - t.Fatalf("API %d: path mismatch: got '%s' - want '%s'", i, apis[i].Path, serverAPIs[i].Path) + if apis[i].Path != gatewayAPIs[i].Path { + t.Fatalf("API %d: path mismatch: got '%s' - want '%s'", i, apis[i].Path, gatewayAPIs[i].Path) } - if apis[i].MaxBody != serverAPIs[i].MaxBody { - t.Fatalf("API %d: max body mismatch: got '%d' - want '%d'", i, apis[i].MaxBody, serverAPIs[i].MaxBody) + if apis[i].MaxBody != gatewayAPIs[i].MaxBody { + t.Fatalf("API %d: max body mismatch: got '%d' - want '%d'", i, apis[i].MaxBody, gatewayAPIs[i].MaxBody) } - if apis[i].Timeout != serverAPIs[i].Timeout { - t.Fatalf("API %d: timeout mismatch: got '%v' - want '%v'", i, apis[i].Timeout, serverAPIs[i].Timeout) + if apis[i].Timeout != gatewayAPIs[i].Timeout { + t.Fatalf("API %d: timeout mismatch: got '%v' - want '%v'", i, apis[i].Timeout, gatewayAPIs[i].Timeout) } } } @@ -102,7 +93,7 @@ func TestCreateKey(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -137,7 +128,7 @@ func TestImportKey(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -169,7 +160,7 @@ func TestGenerateKey(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -220,7 +211,7 @@ func TestEncryptKey(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -280,7 +271,7 @@ func TestDecryptKey(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -355,7 +346,7 @@ func TestDecryptKeyAll(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() @@ -391,57 +382,6 @@ func TestDecryptKeyAll(t *testing.T) { } } -var setPolicyTests = []struct { - Name string - Policy *kes.Policy - ShouldFail bool - Err error -}{ - {Name: "my-policy", Policy: &kes.Policy{}}, - { - Name: "my-policy2", - Policy: &kes.Policy{ - Allow: []string{"/v1/key/create/*", "/v1/key/generate/*"}, - }, - }, - { - Name: "my-policy2", - Policy: &kes.Policy{ - Allow: []string{"/v1/key/create/*", "/v1/key/generate/*"}, - Deny: []string{"/v1/key/create/my-key2"}, - }, - }, - { - Name: "fail-policy", - Policy: &kes.Policy{ - Allow: []string{strings.Repeat("a", 1<<20)}, - }, - ShouldFail: true, - }, -} - -func TestSetPolicy(t *testing.T) { - ctx, cancel := testingContext(t) - defer cancel() - - server := kestest.NewServer() - defer server.Close() - - client := server.Client() - for i, test := range setPolicyTests { - err := client.SetPolicy(ctx, test.Name, test.Policy) - if err == nil && test.ShouldFail { - t.Fatalf("Test %d: should fail but succeeded", i) - } - if err != nil && !test.ShouldFail { - t.Fatalf("Test %d: failed to set policy: %v", i, err) - } - if test.ShouldFail && test.Err != nil && err != test.Err { - t.Fatalf("Test %d: expected to fail with: '%v' - got: '%v'", i, test.Err, err) - } - } -} - var getPolicyTests = []struct { Name string Policy *kes.Policy @@ -466,27 +406,28 @@ func TestDescribePolicy(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() - defer server.Close() - - client := server.Client() for i, test := range getPolicyTests { - if err := client.SetPolicy(ctx, test.Name, test.Policy); err != nil { - t.Fatalf("Test %d: failed to create policy: %v", i, err) - } - info, err := client.DescribePolicy(ctx, test.Name) - if err != nil { - t.Fatalf("Test %d: failed to describe policy: %v", i, err) - } - if info.Name != test.Name { - t.Fatalf("Test %d: policy name mismatch: got '%s' - want '%s'", i, info.Name, test.Name) - } - if info.CreatedAt.IsZero() { - t.Fatalf("Test %d: created_at timestamp not set", i) - } - if info.CreatedBy.IsUnknown() { - t.Fatalf("Test %d: created_by identity not set", i) - } + t.Run(fmt.Sprintf("Test %d", i), func(t *testing.T) { + server := kestest.NewGateway() + defer server.Close() + + server.Policy().Add(test.Name, test.Policy) + client := server.Client() + + info, err := client.DescribePolicy(ctx, test.Name) + if err != nil { + t.Fatalf("Test %d: failed to describe policy: %v", i, err) + } + if info.Name != test.Name { + t.Fatalf("Test %d: policy name mismatch: got '%s' - want '%s'", i, info.Name, test.Name) + } + if info.CreatedAt.IsZero() { + t.Fatalf("Test %d: created_at timestamp not set", i) + } + if info.CreatedBy.IsUnknown() { + t.Fatalf("Test %d: created_by identity not set", i) + } + }) } } @@ -494,51 +435,52 @@ func TestGetPolicy(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() - defer server.Close() - - client := server.Client() for i, test := range getPolicyTests { - if err := client.SetPolicy(ctx, test.Name, test.Policy); err != nil { - t.Fatalf("Test %d: failed to create policy: %v", i, err) - } - policy, err := client.GetPolicy(ctx, test.Name) - if err != nil { - t.Fatalf("Test %d: failed to describe policy: %v", i, err) - } - if policy.Info.Name != test.Name { - t.Fatalf("Policy name mismatch: got '%s' - want '%s'", policy.Info.Name, test.Name) - } - if policy.Info.Name != test.Name { - t.Fatalf("Test %d: policy name mismatch: got '%s' - want '%s'", i, policy.Info.Name, test.Name) - } - if policy.Info.CreatedAt.IsZero() { - t.Fatalf("Test %d: created_at timestamp not set", i) - } - if policy.Info.CreatedBy.IsUnknown() { - t.Fatalf("Test %d: created_by identity not set", i) - } + t.Run(fmt.Sprintf("Test %d", i), func(t *testing.T) { + server := kestest.NewGateway() + defer server.Close() - if len(policy.Allow) != len(test.Policy.Allow) { - t.Fatalf("Test %d: allow policy mismatch: got len %d - want len %d", i, len(policy.Allow), len(test.Policy.Allow)) - } - sort.Strings(test.Policy.Allow) - sort.Strings(policy.Allow) - for j := range policy.Allow { - if policy.Allow[j] != test.Policy.Allow[j] { - t.Fatalf("Test %d: allow policy mismatch: got '%s' - want '%s'", i, policy.Allow[j], test.Policy.Allow[j]) + server.Policy().Add(test.Name, test.Policy) + client := server.Client() + + policy, err := client.GetPolicy(ctx, test.Name) + if err != nil { + t.Fatalf("Test %d: failed to describe policy: %v", i, err) } - } - if len(policy.Deny) != len(test.Policy.Deny) { - t.Fatalf("Test %d: deny policy mismatch: got len %d - want len %d", i, len(policy.Deny), len(test.Policy.Deny)) - } - sort.Strings(test.Policy.Deny) - sort.Strings(policy.Deny) - for j := range policy.Deny { - if policy.Deny[j] != test.Policy.Deny[j] { - t.Fatalf("Test %d: deny policy mismatch: got '%s' - want '%s'", i, policy.Deny[j], test.Policy.Deny[j]) + if policy.Info.Name != test.Name { + t.Fatalf("Policy name mismatch: got '%s' - want '%s'", policy.Info.Name, test.Name) } - } + if policy.Info.Name != test.Name { + t.Fatalf("Test %d: policy name mismatch: got '%s' - want '%s'", i, policy.Info.Name, test.Name) + } + if policy.Info.CreatedAt.IsZero() { + t.Fatalf("Test %d: created_at timestamp not set", i) + } + if policy.Info.CreatedBy.IsUnknown() { + t.Fatalf("Test %d: created_by identity not set", i) + } + + if len(policy.Allow) != len(test.Policy.Allow) { + t.Fatalf("Test %d: allow policy mismatch: got len %d - want len %d", i, len(policy.Allow), len(test.Policy.Allow)) + } + sort.Strings(test.Policy.Allow) + sort.Strings(policy.Allow) + for j := range policy.Allow { + if policy.Allow[j] != test.Policy.Allow[j] { + t.Fatalf("Test %d: allow policy mismatch: got '%s' - want '%s'", i, policy.Allow[j], test.Policy.Allow[j]) + } + } + if len(policy.Deny) != len(test.Policy.Deny) { + t.Fatalf("Test %d: deny policy mismatch: got len %d - want len %d", i, len(policy.Deny), len(test.Policy.Deny)) + } + sort.Strings(test.Policy.Deny) + sort.Strings(policy.Deny) + for j := range policy.Deny { + if policy.Deny[j] != test.Policy.Deny[j] { + t.Fatalf("Test %d: deny policy mismatch: got '%s' - want '%s'", i, policy.Deny[j], test.Policy.Deny[j]) + } + } + }) } } @@ -570,7 +512,7 @@ func TestSelfDescribe(t *testing.T) { ctx, cancel := testingContext(t) defer cancel() - server := kestest.NewServer() + server := kestest.NewGateway() defer server.Close() client := server.Client() diff --git a/kestest/policy.go b/kestest/policy.go index 372c1a56..6fea1a97 100644 --- a/kestest/policy.go +++ b/kestest/policy.go @@ -35,8 +35,10 @@ func (p *PolicySet) Admin() kes.Identity { return p.admin } // Any existing policy with the same name is replaced. func (p *PolicySet) Add(name string, policy *kes.Policy) { p.policies[name] = &auth.Policy{ - Allow: policy.Allow, - Deny: policy.Deny, + Allow: policy.Allow, + Deny: policy.Deny, + CreatedAt: time.Now().UTC(), + CreatedBy: p.admin, } } diff --git a/kestest/server.go b/kestest/server.go index 55901de4..7e025032 100644 --- a/kestest/server.go +++ b/kestest/server.go @@ -2,221 +2,4 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -// Package kestest provides utilities for end-to-end -// KES testing. package kestest - -import ( - "crypto" - "crypto/ed25519" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "io" - "math/big" - "net" - "net/http/httptest" - "time" - - "github.com/minio/kes" - "github.com/minio/kes/internal/auth" - xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/internal/key" - "github.com/minio/kes/internal/log" - "github.com/minio/kes/internal/mem" - "github.com/minio/kes/internal/metric" - "github.com/minio/kes/internal/sys" -) - -// NewServer starts and returns a new Server. -// The caller should call Close when finished, -// to shut it down. -func NewServer() *Server { - s := &Server{} - s.start() - return s -} - -// A Server is a KES server listening on a system-chosen -// port on the local loopback interface, for use in -// end-to-end tests. -type Server struct { - URL string // URL is the base URL of the form https://ipaddr:port. - - policies *PolicySet - client *kes.Client - - caPrivateKey crypto.PrivateKey - caCertificate *x509.Certificate - - server *httptest.Server -} - -// Client returns a KES client configured for making requests -// to the server as admin identity. -// -// It is configured to trust the server's TLS test certificate. -func (s *Server) Client() *kes.Client { return s.client } - -// Policy returns the PolicySet that contains all KES policies -// and identity-policy associations. -func (s *Server) Policy() *PolicySet { return s.policies } - -// Close shuts down the server and blocks until all outstanding -// requests on this server have completed. -func (s *Server) Close() { s.server.Close() } - -// IssueClientCertificate returns a new TLS certificate for -// client authentication with the given common name. -// -// The returned certificate is issued by a testing CA that is -// trusted by the Server. -func (s *Server) IssueClientCertificate(name string) tls.Certificate { - if s.caCertificate == nil || s.caPrivateKey == nil { - s.caPrivateKey, s.caCertificate = newCA() - } - return issueCertificate(name, s.caCertificate, s.caPrivateKey, x509.ExtKeyUsageClientAuth) -} - -// CAs returns the Server's root CAs. -func (s *Server) CAs() *x509.CertPool { - if s.caCertificate == nil || s.caPrivateKey == nil { - s.caPrivateKey, s.caCertificate = newCA() - } - - certpool := x509.NewCertPool() - certpool.AddCert(s.caCertificate) - return certpool -} - -func (s *Server) start() { - if s.caPrivateKey == nil || s.caCertificate == nil { - s.caPrivateKey, s.caCertificate = newCA() - } - - rootCAs := x509.NewCertPool() - rootCAs.AddCert(s.caCertificate) - - var ( - auditLog = log.NewTarget(io.Discard) - errorLog = log.NewTarget(io.Discard) - metrics = metric.New() - adminCert = s.IssueClientCertificate("kestest: admin") - ) - s.policies = &PolicySet{ - admin: Identify(&adminCert), - policies: make(map[string]*auth.Policy), - identities: make(map[kes.Identity]auth.IdentityInfo), - } - - errorLog.Add(metrics.ErrorEventCounter()) - auditLog.Add(metrics.AuditEventCounter()) - store := key.NewCache(&mem.Store{}, &key.CacheConfig{ - Expiry: 30 * time.Second, - ExpiryUnused: 5 * time.Second, - }) - - serverCert := issueCertificate("kestest: server", s.caCertificate, s.caPrivateKey, x509.ExtKeyUsageServerAuth) - s.server = httptest.NewUnstartedServer(xhttp.NewServerMux(&xhttp.ServerConfig{ - Vault: sys.NewStatelessVault(Identify(&adminCert), store, s.policies.policySet(), s.policies.identitySet()), - Proxy: nil, - AuditLog: auditLog, - ErrorLog: errorLog, - Metrics: metrics, - })) - s.server.TLS = &tls.Config{ - RootCAs: rootCAs, - ClientCAs: rootCAs, - Certificates: []tls.Certificate{serverCert}, - ClientAuth: tls.RequireAndVerifyClientCert, - } - s.server.StartTLS() - s.URL = s.server.URL - - s.client = kes.NewClientWithConfig(s.URL, &tls.Config{ - Certificates: []tls.Certificate{adminCert}, - RootCAs: rootCAs, - }) -} - -func newCA() (crypto.PrivateKey, *x509.Certificate) { - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(fmt.Sprintf("kestest: failed to generate CA private key: %v", err)) - } - - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - panic(fmt.Sprintf("kestest: failed to generate CA certificate serial number: %v", err)) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: "kestest Root CA", - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - IsCA: true, - } - certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey) - if err != nil { - panic(fmt.Sprintf("kestest: failed to generate CA certificate: %v", err)) - } - certificate, err := x509.ParseCertificate(certBytes) - if err != nil { - panic(fmt.Sprintf("kestest: failed to generate CA certificate: %v", err)) - } - return privateKey, certificate -} - -func issueCertificate(name string, caCert *x509.Certificate, caKey crypto.PrivateKey, extKeyUsage ...x509.ExtKeyUsage) tls.Certificate { - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(fmt.Sprintf("kestest: failed to generate private/public key pair: %v", err)) - } - - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - panic(fmt.Sprintf("kestest: failed to generate certificate serial number: %v", err)) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: name, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: extKeyUsage, - IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, - DNSNames: []string{"localhost"}, - BasicConstraintsValid: true, - } - - rawCertificate, err := x509.CreateCertificate(rand.Reader, &template, caCert, publicKey, caKey) - if err != nil { - panic(fmt.Sprintf("kestest: failed to create certificate: %v", err)) - } - rawPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey) - if err != nil { - panic(fmt.Sprintf("kestest: failed to create certificate: %v", err)) - } - certificate, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: rawCertificate, - }), pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: rawPrivateKey, - })) - if err != nil { - panic(fmt.Sprintf("kestest: failed to create certificate: %v", err)) - } - return certificate -}