diff --git a/feature/gnsi/certz/tests/server_certificate_rotation/README.md b/feature/gnsi/certz/tests/server_certificate_rotation/README.md index 6e13f3878d8..66e30515918 100644 --- a/feature/gnsi/certz/tests/server_certificate_rotation/README.md +++ b/feature/gnsi/certz/tests/server_certificate_rotation/README.md @@ -66,34 +66,6 @@ Perform this test with both the RSA and ECDSA types. connections to the service impaired / restarted / delayed due to the rotation event. - -### Certz-3.2 - -Perform these negative tests: - -Test that a server certificate can be rotated by using the gNSI certz Rotate() -api if the certificate is requested without the device generated CSR, expect a -failure because the certificate loaded is not signed by a trusted CA. - -Perform this test with both the RSA and ECDSA types. - - 0) Build the test data, configure the DUT to use the ca-0001 form - key/certificate/trust_bundle, use the server-${TYPE}-a key/certificate. - - 1) With the server running, connect and note that the ceritficate loaded - is the appropriate one. - - 2) Use the gNSI Rotate RPC to load a ca-02/server-${TYPE}-b key and - certificate on to the server. - - 3) Test that the certificate load fails, because the certificate is not - trusted by a known CA. - - 4) Tear down the Rotate RPC, forcing the device to return to the - previously used certificate/key material. - - 5) Verify that the server is now serving the previous certifcate properly. - ## OpenConfig Path and RPC Coverage The below yaml defines the OC paths intended to be covered by this test. OC paths used for test setup are not listed here. @@ -106,6 +78,15 @@ rpcs: certz.v1.Certz.Rotate: ``` +## Canonical OC +No OC configuration is performed in this test as all interaction is via gNSI.Certz + +```json +{ + +} +``` + ## Minimum DUT Platform Requirement vRX diff --git a/feature/gnsi/certz/tests/server_certificate_rotation/server_certificate_rotation_test.go b/feature/gnsi/certz/tests/server_certificate_rotation/server_certificate_rotation_test.go new file mode 100644 index 00000000000..e73cfdc65c8 --- /dev/null +++ b/feature/gnsi/certz/tests/server_certificate_rotation/server_certificate_rotation_test.go @@ -0,0 +1,245 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server_certificate_rotation_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "slices" + "testing" + "time" + + setupService "github.com/openconfig/featureprofiles/feature/gnsi/certz/tests/internal/setup_service" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/gnmi/proto/gnmi" + certzpb "github.com/openconfig/gnsi/certz" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/binding" +) + +const ( + dirPath = "../../test_data/" + timeOutVar time.Duration = 2 * time.Minute +) + +// DUTCredentialer is an interface for getting credentials from +type DUTCredentialer interface { + RPCUsername() string + RPCPassword() string +} + +var ( + serverAddr string + creds DUTCredentialer //an interface for getting credentials from a DUT binding + testProfile string = "newprofile" //sslProfileId name + prevClientCertFile string = "" + prevClientKeyFile string = "" + prevTrustBundleFile string = "" + logTime string = time.Now().String() //Timestamp + expectedResult bool = true + success bool +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +// TestServerCertRotation tests a server certificate can be rotated by using the gNSI certz Rotate() rpc, +// if the certificate is requested without the device generated CSR. +func TestServerCertRotation(t *testing.T) { + + dut := ondatra.DUT(t, "dut") + serverAddr = dut.Name() //returns the device name. + if err := binding.DUTAs(dut.RawAPIs().BindingDUT(), &creds); err != nil { + t.Fatalf("%s:STATUS:Failed to get DUT credentials using binding.DUTAs: %v. The binding for %s must implement the DUTCredentialer interface.", logTime, err, dut.Name()) + } + username := creds.RPCUsername() + password := creds.RPCPassword() + t.Logf("%s:STATUS:Validation of all services that are using gRPC before certz rotation.", logTime) + gnmiClient, gnsiC := setupService.PreInitCheck(context.Background(), t, dut) + //Generate testdata certificates. + t.Logf("%s:Creation of test data.", logTime) + if err := setupService.TestdataMakeCleanup(t, dirPath, timeOutVar, "./mk_cas.sh"); err != nil { + t.Fatalf("%s:STATUS:Generation of testdata certificates failed!: %v", logTime, err) + } + //Create a certz client. + ctx := context.Background() + certzClient := gnsiC.Certz() + t.Logf("%s:STATUS:Precheck:checking baseline sslprofile list.", logTime) + //Get sslprofile list. + if getResp := setupService.GetSslProfilelist(ctx, t, certzClient, &certzpb.GetProfileListRequest{}); slices.Contains(getResp.SslProfileIds, testProfile) { + t.Fatalf("%s:STATUS:profileID %s already exists.", logTime, testProfile) + } + //Add new sslprofileID. + t.Logf("%s:Adding new empty sslprofile ID %s.", logTime, testProfile) + if addProfileResponse, err := certzClient.AddProfile(ctx, &certzpb.AddProfileRequest{SslProfileId: testProfile}); err != nil { + t.Fatalf("%s:STATUS:Add profile request failed with %v! ", logTime, err) + } else { + t.Logf("%s:STATUS:Received the AddProfileResponse %v.", logTime, addProfileResponse) + } + //Get sslprofile list after new sslprofile addition. + if getResp := setupService.GetSslProfilelist(ctx, t, certzClient, &certzpb.GetProfileListRequest{}); !slices.Contains(getResp.SslProfileIds, testProfile) { + t.Fatalf("%s:STATUS:newly added profileID is not seen.", logTime) + } else { + t.Logf("%s:STATUS:new profileID %s is seen in sslprofile list", logTime, testProfile) + } + cases := []struct { + desc string + serverCertFile string + serverKeyFile string + trustBundleFile string + clientCertFile string + clientKeyFile string + cversion string + bversion string + newTLScreds bool + serverCertOnlyRotate bool + mismatch bool + scale bool + }{ + { + desc: "Certz3.1:Rotate server-rsa-a certificate/key/trustbundle from ca-01", + serverCertFile: dirPath + "ca-01/server-rsa-a-cert.pem", + serverKeyFile: dirPath + "ca-01/server-rsa-a-key.pem", + trustBundleFile: dirPath + "ca-01/trust_bundle_01_rsa.p7b", + clientCertFile: dirPath + "ca-01/client-rsa-a-cert.pem", + clientKeyFile: dirPath + "ca-01/client-rsa-a-key.pem", + cversion: "v1", + bversion: "bundle1", + }, + { + desc: "Certz3.1:Rotate server-rsa-b certificate/key/trustbundle from ca-01", + serverCertFile: dirPath + "ca-01/server-rsa-b-cert.pem", + serverKeyFile: dirPath + "ca-01/server-rsa-b-key.pem", + trustBundleFile: dirPath + "ca-01/trust_bundle_01_rsa.p7b", + clientCertFile: dirPath + "ca-01/client-rsa-b-cert.pem", + clientKeyFile: dirPath + "ca-01/client-rsa-b-key.pem", + cversion: "v2", + bversion: "bundle1", + serverCertOnlyRotate: true, + newTLScreds: true, + }, + { + desc: "Certz3.1:Rotate server-ecdsa-a certificate/key/trustbundle from ca-01", + serverCertFile: dirPath + "ca-01/server-ecdsa-a-cert.pem", + serverKeyFile: dirPath + "ca-01/server-ecdsa-a-key.pem", + trustBundleFile: dirPath + "ca-01/trust_bundle_01_ecdsa.p7b", + clientCertFile: dirPath + "ca-01/client-ecdsa-a-cert.pem", + clientKeyFile: dirPath + "ca-01/client-ecdsa-a-key.pem", + cversion: "v3", + bversion: "bundle2", + newTLScreds: true, + }, + { + desc: "Certz3.1:Rotate server-ecdsa-b certificate/key/trustbundle from ca-01", + serverCertFile: dirPath + "ca-01/server-ecdsa-b-cert.pem", + serverKeyFile: dirPath + "ca-01/server-ecdsa-b-key.pem", + trustBundleFile: dirPath + "ca-01/trust_bundle_01_ecdsa.p7b", + clientCertFile: dirPath + "ca-01/client-ecdsa-b-cert.pem", + clientKeyFile: dirPath + "ca-01/client-ecdsa-b-key.pem", + cversion: "v4", + bversion: "bundle2", + serverCertOnlyRotate: true, + newTLScreds: true, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + t.Logf("%s:STATUS:Starting test case: %s", logTime, tc.desc) + //Read the serverSAN (Subject Alternative Name) from the certificate used for TLS verification. + serverSAN := setupService.ReadDecodeServerCertificate(t, tc.serverCertFile) + //Build serverCertEntity for the server certificate rotation. + serverCert := setupService.CreateCertzChain(t, setupService.CertificateChainRequest{ + RequestType: setupService.EntityTypeCertificateChain, + ServerCertFile: tc.serverCertFile, + ServerKeyFile: tc.serverKeyFile}) + serverCertEntity := setupService.CreateCertzEntity(t, setupService.EntityTypeCertificateChain, &serverCert, tc.cversion) + //Create a new Cert Pool and add the certs from the trust bundle. + pkcs7certs, pkcs7data, err := setupService.Loadpkcs7TrustBundle(tc.trustBundleFile) + if err != nil { + t.Fatalf("%s:STATUS:failed to load trust bundle: %v", logTime, err) + } + newCaCert := x509.NewCertPool() + for _, c := range pkcs7certs { + newCaCert.AddCert(c) + } + //Build trustBundleEntity for the server certificate rotation. + trustBundleEntity := setupService.CreateCertzEntity(t, setupService.EntityTypeTrustBundle, string(pkcs7data), tc.bversion) + //Load Client certificate. + newClientCert, err := tls.LoadX509KeyPair(tc.clientCertFile, tc.clientKeyFile) + if err != nil { + t.Fatalf("%s:STATUS:Failed to load client cert:%v", logTime, err) + } + if tc.newTLScreds { + t.Logf("%s:STATUS:%s:Creating new TLS credentials for client connection.", logTime, tc.desc) + //Load the prior client keypair for new client TLS credentials. + prevClientCert, err := tls.LoadX509KeyPair(prevClientCertFile, prevClientKeyFile) + if err != nil { + t.Fatalf("%s:STATUS:%s:Failed to load previous client cert: %v", logTime, tc.desc, err) + } + oldPkcs7certs, oldPkcs7data, err := setupService.Loadpkcs7TrustBundle(prevTrustBundleFile) + if err != nil { + t.Fatalf("%s:STATUS:%sFailed to load previous trust bundle,data %v with %v", logTime, tc.desc, oldPkcs7data, err) + } + //Create a old set of Cert Pool and append the certs from previous trust bundle. + prevCaCert := x509.NewCertPool() + for _, c := range oldPkcs7certs { + prevCaCert.AddCert(c) + } + //Before rotation, validation of all services with existing certificates. + if result := setupService.ServicesValidationCheck(t, prevCaCert, expectedResult, serverSAN, serverAddr, username, password, prevClientCert, tc.mismatch); !result { + t.Fatalf("%s:STATUS:%s:service validation failed before rotate- got %v, want %v.", logTime, tc.desc, result, expectedResult) + } + //Retrieve the connection with previous TLS credentials for certz rotation. + conn := setupService.CreateNewDialOption(t, prevClientCert, prevCaCert, serverSAN, username, password, serverAddr) + defer conn.Close() + certzClient = certzpb.NewCertzClient(conn) + gnmiClient = gnmi.NewGNMIClient(conn) + } else { + t.Logf("%s:STATUS:%s:Using existing TLS credentials for client connection in first iteration.", logTime, tc.desc) + } + //Initiate server certitificate rotation. + if tc.serverCertOnlyRotate { + t.Logf("%s:STATUS:%s:Initiating server certificate rotation to server-${TYPE}-b.", logTime, tc.desc) + if success = setupService.CertzRotate(ctx, t, newCaCert, certzClient, gnmiClient, newClientCert, dut, username, password, serverSAN, serverAddr, testProfile, tc.newTLScreds, tc.mismatch, tc.scale, &serverCertEntity); !success { + t.Fatalf("%s STATUS %s:Server certificate rotation failed.", logTime, tc.desc) + } + } else { + t.Logf("%s:STATUS:%s Initiating Certz rotation with server cert: %s and trust bundle: %s", logTime, tc.desc, tc.serverCertFile, tc.trustBundleFile) + if success = setupService.CertzRotate(ctx, t, newCaCert, certzClient, gnmiClient, newClientCert, dut, username, password, serverSAN, serverAddr, testProfile, tc.newTLScreds, tc.mismatch, tc.scale, &serverCertEntity, &trustBundleEntity); !success { + t.Fatalf("%s STATUS %s:Server certificate rotation failed.", logTime, tc.desc) + } + } + t.Logf("%s:STATUS:%s:Server certificate rotation completed!", logTime, tc.desc) + t.Run("Verification of new connection after successful server certificate rotation", func(t *testing.T) { + if result := setupService.ServicesValidationCheck(t, newCaCert, expectedResult, serverSAN, serverAddr, username, password, newClientCert, tc.mismatch); !result { + t.Fatalf("%s:STATUS:%s:service validation failed after rotate- got %v, want %v.", logTime, tc.desc, result, expectedResult) + } + t.Logf("%s:STATUS:%s:service validation done!", logTime, tc.desc) + }) + //Archiving previous client cert/key and trustbundle. + prevClientCertFile = tc.clientCertFile + prevClientKeyFile = tc.clientKeyFile + prevTrustBundleFile = tc.trustBundleFile + }) + } + t.Logf("%s:STATUS:Cleanup of test data.", logTime) + //Cleanup of test data. + if err := setupService.TestdataMakeCleanup(t, dirPath, timeOutVar, "./cleanup.sh"); err != nil { + t.Logf("%s:STATUS:Cleanup of testdata certificates failed!: %v", logTime, err) + } + t.Logf("%s:STATUS: Testdata cleanup completed!", logTime) +}