From 47895b4f377fa592e2eec39178efb22595b0b907 Mon Sep 17 00:00:00 2001 From: "Dipesh Chauhan (Nokia)" Date: Fri, 15 Nov 2024 12:57:08 -0500 Subject: [PATCH] Attestz-1 "This code is a Contribution to the OpenConfig Feature Profiles project ("Work") made under the Google Software Grant and Corporate Contributor License Agreement ("CLA") and governed by the Apache License 2.0. No other rights or licenses in or to any of Nokia's intellectual property are granted for any other purpose. This code is provided on an "as is" basis without any warranties of any kind." --- .../system/attestz/tests/attestz1/README.md | 65 +++ .../attestz/tests/attestz1/attestz1_test.go | 276 ++++++++++ .../attestz/tests/attestz1/metadata.textproto | 7 + .../attestz/tests/testdata/nokia-ca.cert.pem | 37 ++ .../attestz/tests/testdata/owner-ca.cert.pem | 22 + .../attestz/tests/testdata/owner-ca.key.pem | 28 + go.mod | 5 +- go.sum | 2 + internal/security/attestz/attestz.go | 517 ++++++++++++++++++ internal/security/attestz/cert.go | 340 ++++++++++++ internal/security/attestz/events.go | 151 +++++ internal/security/attestz/setup.go | 204 +++++++ 12 files changed, 1652 insertions(+), 2 deletions(-) create mode 100644 feature/system/attestz/tests/attestz1/README.md create mode 100644 feature/system/attestz/tests/attestz1/attestz1_test.go create mode 100644 feature/system/attestz/tests/attestz1/metadata.textproto create mode 100644 feature/system/attestz/tests/testdata/nokia-ca.cert.pem create mode 100644 feature/system/attestz/tests/testdata/owner-ca.cert.pem create mode 100644 feature/system/attestz/tests/testdata/owner-ca.key.pem create mode 100644 internal/security/attestz/attestz.go create mode 100644 internal/security/attestz/cert.go create mode 100644 internal/security/attestz/events.go create mode 100644 internal/security/attestz/setup.go diff --git a/feature/system/attestz/tests/attestz1/README.md b/feature/system/attestz/tests/attestz1/README.md new file mode 100644 index 00000000000..18944a0b09c --- /dev/null +++ b/feature/system/attestz/tests/attestz1/README.md @@ -0,0 +1,65 @@ +# attestz-1: Validate attestz for initial install + +## Summary + +In TPM enrollment workflow the switch owner verifies device's Initial Attestation Key (IAK) and Initial DevID (IDevID) certificates (signed by the switch vendor CA) and installs/rotates owner IAK (oIAK) and owner IDevID (oIDevID) certificates (signed by switch owner CA). In TPM attestation workflow switch owner verifies that the device's end-to-end boot state (bootloader, OS, secure boot policy, etc.) matches owner's expectations. + +## Procedure + +Test should verify all success and failure/corner-case scenarios for TPM enrollment and attestation workflows that are specified in [attestz Readme](https://github.com/openconfig/attestz/blob/main/README.md). + +TPM enrollment workflow consists of two APIs defined in openconfig/attestz/blob/main/proto/tpm_enrollz.proto: `GetIakCert` and `RotateOIakCert`. +TPM attestation workflow consists of a single API defined in openconfig/attestz/blob/main/proto/tpm_attestz.proto: `Attest`. +The tests should comprehensively cover the behavior for all three APIs when used both separately and sequentially. +Finally, the tests should cover both initial install/bootstrapping, oIAK/oIDevID rotation and post-install re-attestation workflows. + +## Test Setup + +1. Switch vendor provisioned the device with IAK and IDevID certs following TCG spec [Section 5.2](https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf#page=20) and [Section 6.2](https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf#page=30). +2. The device successfully completed the bootz workflow where it obtained and applied all configurations/credentials/certificates and booted into the right OS image. +3. Device is serving `enrollz` and `attestz` gRPC endpoints. + +### attestz-1: Validate attestz for initial install + +The test validates that the device completes TPM enrollment and attestation during initial device bootstrapping/install. + +| ID | Case | Result | +| --- | ---- | ------ | +| attestz-1.1 | Successful enrollment and attestation | Device obtained oIAK and oIDevID certs, updated default SSL profile to rely on the oIDevID cert, and passed attestation for all control cards | +| attestz-1.2 | IAK/IDevID are not present on the device | `GetIakCert` fails with missing IAK/IDevID error | +| attestz-1.3 | Bad request for `GetIakCertRequest`, `RotateOIakCertRequest` and  `AttestRequest`. Examples: `ControlCardSelection control_card_selection` is not specified or `control_card_id.role = 0`. Invalid `control_card_id.serial` or `control_card_id.slot` | `GetIakCert`, `RotateOIakCert` and `Attest` fail with detailed invalid request error | +| attestz-1.4 | Store oIAK/oIDevId certs that have different underlying IAK/IDevID pub keys or intended for other control card | `RotateOIakCert` fails with detailed invalid request error | +| attestz-1.5 | `enrollz` workflow followed by a device reboot still results in a successful `attestz` workflow | Device obtained oIAK and oIDevID certs and passed attestation for all control cards | +| attestz-1.6 | Full factory reset of the device after a successful `enrollz` workflow deletes oIAK and oIDevID certs, but does not affect IAK and IDevID certs | After factory reset the device fails to perform `attestz` workflow due to missing oIAK and oIDevID certs | +| attestz-1.7 | Out of bound or repeated `pcr_indices` in `AttestRequest` | `Attest` fails with detailed invalid request error | +| attestz-1.8 | RMA scenarios where an active control card ensures that a newly inserted standby control card completes TPM enrollment and attestation before obtaining **its own set** of owner-issued production credentials/certificates (no sharing of owner-issued production security artifacts is allowed between control cards) | `attestz` on a newly inserted control card fails before the card successfully completes TPM enrollment workflow; all RPCs relying on owner-issued credentials/certs fail on a newly inserted control card before the card successfully completes TPM enrollment and attestation workflows | +| attestz-1.9 | Regardless of which control card was active during `enrollz`, both control cards should be able to successfully complete `attestz` workflow as active control cards | Device obtained oIAK and oIDevID certs and passed attestation for all control cards | + +1. Call `GetIakCert` for an active control card with correct `ControlCardSelection`. +2. Verify that correct IDevID cert was used for establishing TLS session: + * Cert structure matches TCG specification [Section 8](https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf#page=55). + * Cert is not expired. + * Cert is signed by switch vendor CA. + * Cert is tied to the active control card. +3. Verify IAK cert: + * Cert structure matches TCG spec (similar to IDevID above). + * Cert is not expired. + * Cert is signed by switch vendor CA. + * Cert is tied to the active control card. + * IAK and IDevID cert contain the same device serial number field. +4. Verify that the device returned the correct `ControlCardVendorId` with all fields populated. +5. Issue owner IAK (oIAK) and owner IDevID (oIDevID) certs, which are based on the same underlying public keys, have the same structure and fields, but are signed by a different - owner - CA. +6. Call `RotateOIakCert` to store newly issued oIAK and oIDevID certs and verify successful response. +7. Call `GetIakCert` for a standby control card with correct `ControlCardSelection`. +8. Repeat step (2) (TLS session will be secured by active control card's IDevID) and verify IDevID cert of standby control card was specified in the response payload. +9. Repeat steps (3-6) for the standby control card. +10. Call `Attest` for active control card with correct `ControlCardSelection`, random nonce, hash algo of choice (all should be supported and tested) and all PCR indices. +11. Verify that the correct oIDevID cert was used for establishing TLS session. +12. Verify that the device returned the correct `ControlCardVendorId` with all fields populated. +13. Verify oIAK cert is the same as the one installed earlier. +14. Verify all `pcr_values` match expectations. +15. Verify `quote_signature` signature with oIAK cert. +16. Use `pcr_values` and `quoted` to recompute PCR Quote digest and verify that it matches the one used in `quote_signature`. +17. Call `Attest` for standby control card with correct `ControlCardSelection`, random nonce, hash algo of choice (all should be supported and tested) and all PCR indices. +18. Verify that the oIDevID cert of active control card was used for establishing TLS session and verify that oIDevID cert of standby control card was specified in the response payload. +19. Repeat steps (12-16) for the standby control card. \ No newline at end of file diff --git a/feature/system/attestz/tests/attestz1/attestz1_test.go b/feature/system/attestz/tests/attestz1/attestz1_test.go new file mode 100644 index 00000000000..722cc34f53f --- /dev/null +++ b/feature/system/attestz/tests/attestz1/attestz1_test.go @@ -0,0 +1,276 @@ +// 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 attestz1 + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + cdpb "github.com/openconfig/attestz/proto/common_definitions" + attestzpb "github.com/openconfig/attestz/proto/tpm_attestz" + enrollzpb "github.com/openconfig/attestz/proto/tpm_enrollz" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/security/attestz" + "github.com/openconfig/featureprofiles/internal/security/svid" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" +) + +const ( + cdpbActive = cdpb.ControlCardRole_CONTROL_CARD_ROLE_ACTIVE + cdpbStandby = cdpb.ControlCardRole_CONTROL_CARD_ROLE_STANDBY +) + +var ( + vendorCaCertPem = flag.String("switch_vendor_ca_cert", "", "a pem file for vendor ca cert used for verifying iDevID/IAK Certs") + ownerCaCertPem = flag.String("switch_owner_ca_cert", "../testdata/owner-ca.cert.pem", "a pem file for ca cert that will be used to sign oDevID/oIAK/mTLS Certs") + ownerCaKeyPem = flag.String("switch_owner_ca_key", "../testdata/owner-ca.key.pem", "a pem file for ca key that will be used to sign oDevID/oIAK/mTLS Certs") +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +func TestAttestz1(t *testing.T) { + dut := ondatra.DUT(t, "dut") + // Retrieve vendor ca certificate from testdata if not provided in test args. + if *vendorCaCertPem == "" { + *vendorCaCertPem = fmt.Sprintf("../testdata/%s-ca.cert.pem", strings.ToLower(dut.Vendor().String())) + } + + attestzTarget, attestzServer := attestz.SetupBaseline(t, dut) + t.Cleanup(func() { + gnmi.Delete(t, dut, gnmi.OC().System().GrpcServer(*attestzServer.Name).Config()) + attestz.DeleteProfile(t, dut, *attestzServer.SslProfileId) + }) + tc := &attestz.TlsConf{ + Target: attestzTarget, + CaKeyFile: *ownerCaKeyPem, + CaCertFile: *ownerCaCertPem, + } + + // Find active and standby card. + activeCard, standbyCard := attestz.FindControlCards(t, dut) + + t.Run("Attestz-1.1 - Successful enrollment and attestation", func(t *testing.T) { + // Enroll for active & standby card. + activeCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + standbyCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + + // Attest for active & standby card. + activeCard.AttestzWorkflow(t, dut, tc) + standbyCard.AttestzWorkflow(t, dut, tc) + }) + + t.Run("Attestz-1.3 - Bad request", func(t *testing.T) { + var as *attestz.AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + invalidSerial := attestz.ParseSerialSelection("000") + + _, err := as.EnrollzClient.GetIakCert(context.Background(), &enrollzpb.GetIakCertRequest{ + ControlCardSelection: invalidSerial, + }) + if err != nil { + t.Logf("Got expected error for GetIakCert bad request %v", err) + } else { + t.Fatal("GetIakCert rpc succeeded but expected to fail.") + } + + attestzRequest := &enrollzpb.RotateOIakCertRequest{ + ControlCardSelection: invalidSerial, + } + _, err = as.EnrollzClient.RotateOIakCert(context.Background(), attestzRequest) + if err != nil { + t.Logf("Got expected error for RotateOIakCert bad request %v", err) + } else { + t.Fatal("RotateOIakCert rpc succeeded but expected to fail.") + } + + _, err = as.AttestzClient.Attest(context.Background(), &attestzpb.AttestRequest{ + ControlCardSelection: invalidSerial, + }) + if err != nil { + t.Logf("Got expected error for Attest bad request %v", err) + } else { + t.Fatal("Attest rpc succeeded but expected to fail.") + } + + }) + + t.Run("Attestz-1.4 - Incorrect Public Key", func(t *testing.T) { + roleA := attestz.ParseRoleSelection(cdpbActive) + roleB := attestz.ParseRoleSelection(cdpbStandby) + + // Get vendor certs. + var as *attestz.AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + resp := as.GetVendorCerts(t, roleA) + activeCard.IAKCert, activeCard.IDevIDCert = resp.IakCert, resp.IdevidCert + resp = as.GetVendorCerts(t, roleB) + standbyCard.IAKCert, standbyCard.IDevIDCert = resp.IakCert, resp.IdevidCert + + caKey, caCert, err := svid.LoadKeyPair(tc.CaKeyFile, tc.CaCertFile) + if err != nil { + t.Fatalf("Could not load ca key/cert. error: %v", err) + } + + // Generate active card's oIAK/oIDevId certs with standby card's public key (to simulate incorrect public key). + standbyIAKCert, err := attestz.LoadCertificate(standbyCard.IAKCert) + if err != nil { + t.Fatalf("Error loading IAK cert for standby card. error: %v", err) + } + t.Logf("Generating oIAK cert for card %v with incorrect public key", activeCard.Name) + oIAKCert := attestz.GenOwnerCert(t, caKey, caCert, activeCard.IAKCert, standbyIAKCert.PublicKey, tc.Target) + + standbyIDevIDCert, err := attestz.LoadCertificate(standbyCard.IDevIDCert) + if err != nil { + t.Fatalf("Error loading IDevID Cert for standby card. error: %v", err) + } + t.Logf("Generating oDevID cert for card %v with incorrect public key", activeCard.Name) + oDevIDCert := attestz.GenOwnerCert(t, caKey, caCert, activeCard.IDevIDCert, standbyIDevIDCert.PublicKey, tc.Target) + + // Verify rotate rpc fails. + attestzRequest := &enrollzpb.RotateOIakCertRequest{ + ControlCardSelection: roleA, + OiakCert: oIAKCert, + OidevidCert: oDevIDCert, + SslProfileId: *attestzServer.SslProfileId, + } + _, err = as.EnrollzClient.RotateOIakCert(context.Background(), attestzRequest) + if err != nil { + t.Logf("Got expected error for RotateOIakCert bad request %v", err) + } else { + t.Fatal("RotateOIakCert rpc succeeded but expected to fail.") + } + }) + + t.Run("Attestz-1.5 - Device Reboot", func(t *testing.T) { + activeCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + standbyCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + + // Trigger reboot. + attestz.RebootDut(t, dut) + t.Logf("Wait for cards to get synchronized after reboot ...") + attestz.SwitchoverReady(t, dut, activeCard.Name, standbyCard.Name) + + // Check active card after reboot & swap control card struct if required. + rr := gnmi.Get[oc.E_Platform_ComponentRedundantRole](t, dut, gnmi.OC().Component(activeCard.Name).RedundantRole().State()) + if rr != oc.Platform_ComponentRedundantRole_PRIMARY { + t.Logf("Card roles have changed. %s is the new active card.", standbyCard.Name) + *activeCard, *standbyCard = *standbyCard, *activeCard + activeCard.Role = cdpbActive + standbyCard.Role = cdpbStandby + } + + // Verify attest workflow after reboot. + activeCard.AttestzWorkflow(t, dut, tc) + standbyCard.AttestzWorkflow(t, dut, tc) + }) + + t.Run("Attestz-1.6 - Factory Reset", func(t *testing.T) { + activeCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + standbyCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + + // Trigger factory reset. + attestz.FactoryResetDut(t, dut) + t.Logf("Wait for cards to get synchronized after factory reset ...") + attestz.SwitchoverReady(t, dut, activeCard.Name, standbyCard.Name) + + // Setup baseline configs again after factory reset (ensure bootz pushes relevant configs used by ondatra bindings). + attestzTarget, attestzServer = attestz.SetupBaseline(t, dut) + activeCard, standbyCard = attestz.FindControlCards(t, dut) + + var as *attestz.AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + + _, err := as.AttestzClient.Attest(context.Background(), &attestzpb.AttestRequest{ + ControlCardSelection: attestz.ParseRoleSelection(cdpbActive), + Nonce: attestz.GenNonce(t), + HashAlgo: attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA256, + PcrIndices: attestz.PcrIndices, + }) + if err != nil { + t.Logf("Got expected error for Attest rpc of active card %s. error: %s", activeCard.Name, err) + } else { + t.Fatalf("Attest rpc for active card %s succeeded but expected to fail.", activeCard.Name) + } + }) + + t.Run("Attestz-1.7 - Invalid PCR indices", func(t *testing.T) { + activeCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + var as *attestz.AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + _, err := as.AttestzClient.Attest(context.Background(), &attestzpb.AttestRequest{ + ControlCardSelection: attestz.ParseRoleSelection(activeCard.Role), + Nonce: attestz.GenNonce(t), + HashAlgo: attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA256, + PcrIndices: []int32{25, -25}, + }) + if err != nil { + t.Logf("Got expected error for Attest bad request %v", err) + } else { + t.Fatal("Expected error in Attest with invalid pcr indices") + } + }) + + // Ensure factory reset test ran before running this test to simulate rma scenario. + t.Run("Attestz-1.8 - Attest failure on standby card", func(t *testing.T) { + // Enroll & attest active card. + activeCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + activeCard.AttestzWorkflow(t, dut, tc) + + var as *attestz.AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + _, err := as.AttestzClient.Attest(context.Background(), &attestzpb.AttestRequest{ + ControlCardSelection: attestz.ParseRoleSelection(cdpbStandby), + Nonce: attestz.GenNonce(t), + HashAlgo: attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA256, + PcrIndices: attestz.PcrIndices, + }) + if err != nil { + t.Logf("Got expected error for Attest rpc of standby card %s. error: %s", standbyCard.Name, err) + } else { + t.Fatalf("Attest rpc for standby card %s succeeded but expected to fail.", standbyCard.Name) + } + }) + + t.Run("Attestz-1.9 - Control Card Switchover", func(t *testing.T) { + activeCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + standbyCard.EnrollzWorkflow(t, dut, tc, *vendorCaCertPem) + + // Trigger control card switchover. + attestz.SwitchoverCards(t, dut, activeCard.Name, standbyCard.Name) + t.Logf("Wait for cards to get synchronized after switchover ...") + attestz.SwitchoverReady(t, dut, activeCard.Name, standbyCard.Name) + + // Swap control card struct after switchover. + *activeCard, *standbyCard = *standbyCard, *activeCard + activeCard.Role = cdpbActive + standbyCard.Role = cdpbStandby + + // Verify attest workflow after switchover. + activeCard.AttestzWorkflow(t, dut, tc) + standbyCard.AttestzWorkflow(t, dut, tc) + }) +} diff --git a/feature/system/attestz/tests/attestz1/metadata.textproto b/feature/system/attestz/tests/attestz1/metadata.textproto new file mode 100644 index 00000000000..28e84158745 --- /dev/null +++ b/feature/system/attestz/tests/attestz1/metadata.textproto @@ -0,0 +1,7 @@ +# proto-file: github.com/openconfig/featureprofiles/proto/metadata.proto +# proto-message: Metadata + +uuid: "49517013-0588-4565-afbc-3ea41d8db257" +plan_id: "attestz-1" +description: "Validate attestz for initial install" +testbed: TESTBED_DUT diff --git a/feature/system/attestz/tests/testdata/nokia-ca.cert.pem b/feature/system/attestz/tests/testdata/nokia-ca.cert.pem new file mode 100644 index 00000000000..056e09a37f1 --- /dev/null +++ b/feature/system/attestz/tests/testdata/nokia-ca.cert.pem @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGYDCCBEigAwIBAgIUePsHg1oV7Q1GdF/9M4IkIAY+F4gwDQYJKoZIhvcNAQEM +BQAwSTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBU5va2lhMSowKAYDVQQDEyFOb2tp +YSBJUCBOZXR3b3JrcyBGYWN0b3J5IFJvb3QgQ0EwIBcNMjMxMDEwMTU1MzU1WhgP +MjA3MzEwMDkxNTM5MDhaMEUxCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVOb2tpYTEm +MCQGA1UEAxMdTm9raWEgSVAgTmV0d29ya3MgRmFjdG9yeSBDQTEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDDfVpJ/ji15e6DdMCrPNogqUOb6zQp1koV +r+TlfUVlh0b3yorrZG4JGhqRc/VQ7qdBx1jpywQa7P6lqDREhWKWGa3UBD2BtyIC +jZaujQIHIM3rzA5qtgitj8YRLEsKvCqL4JsdMbsqGZPYC2Q7lWRJqSZwfnhx0vPm +hYOjQn+gbnzWDbKbHkYUTsS6eC4ReVYEk4YlxcjJcabRb0w3OAj3i3u1F2T0F37b +JUQWu94Pf4J9o8/thEdt2LfCeKDxbwX72q4zymGo77zruh0bHhi0KbsKGA6uF1+p +sw2TjnDo19GzjpEW+ySp07fYGapke2oKpIsJJ0DUmjg/WL0lFZa3BTBzTDHZq4nu +cE5B2zqkdiq7ra6ozMmM0aBIqUMYrvjOtdXWniQL6VFlnmHD1+pUFUm1b3BUwCP3 +ydTwzKk1E7xBVb88aInOOVO08WyYBEJ8t8Rap8P1TWjkXrh3YCuxACgiqx+E02ct +lXYX4m6XKD8moMe81AGoM1O5ATv+phPHwpSNoEtBIjk1DX2bdNwVnBBKq/8yh7ey +o9sH9TK9SHCHaZt44Uwxqw7X885/MlbMwI/lFX9JRfx1o8qkN8lUWPvQ76nV+PKp +shBq2J2FzWnm+HdMMYlMQav8C1V2LBjWTWna6iNv+1We0lMJZno31v4xlCwYOQqe +Xvzafx3xeQIDAQABo4IBQDCCATwwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQUOhNBkE077lmEbT2/UGPOUjVk8yswHwYDVR0jBBgwFoAU/yqnjcq4acjk81OE +20uGv2+Gxl0wDgYDVR0PAQH/BAQDAgGGMIGGBggrBgEFBQcBAQR6MHgwKAYIKwYB +BQUHMAGGHGh0dHA6Ly9vY3NwLm9uZS5kaWdpY2VydC5jb20wTAYIKwYBBQUHMAKG +QGh0dHA6Ly9jYWNlcnRzLm9uZS5kaWdpY2VydC5jb20vTm9raWFJUE5ldHdvcmtz +RmFjdG9yeVJvb3RDQS5jcnQwTQYDVR0fBEYwRDBCoECgPoY8aHR0cDovL2NybC5v +bmUuZGlnaWNlcnQuY29tL05va2lhSVBOZXR3b3Jrc0ZhY3RvcnlSb290Q0EuY3Js +MA0GCSqGSIb3DQEBDAUAA4ICAQCVYEVn/RyiM3ybOn1+BA0+0sqBiLM6I6BBPGuq +9JQvdHETIkdd5VI2/5BLc1ZcBatLmlJkYW8rUDfZLU1Hv8IHpBbH6TWEBjNo5mtF +aY4UhHWiEOUAP5cboxIjR8F2SmSONvIMvIIoG8HKr6UupQx264TxMgdKmctjbAuI +BcCitZNEG0M6l4bwI4dlbbEuI0sh4l5TpThmzXrYSt7zyvrpajRmcwHFpX9tq6aH +dt2FEELY/JLQaZvVnIHcdWnhMyG1nonALbxV8HWC6gr9mwoBWLRK9tcwsyw2q88R +J1HevqJWABSdwVydh5OhnViLq9i8u3BxnMegzqM6R8mYPHTaZSl1FXb6uQmp6V2y +40kg4D8sSn5jkLtovStu7uqAGTZTR1/Ld2KF3fSv8ZxeX+ia4lPRxSpoOTXeTIfa +E7bIPBVhw636gEs/Ztz6T90Pzs5qNhQ1bQlFfqBlZyXmG2q2PwJwcQkjiuJ4LWNg +JO1p8Es6E/wy3HB9ymY4dpSqoyd1ZkGEKA0+L1AFkhizUsi7EgGge9Il49AtQiyp +lvuJ0m+94MEaNVJUxMVVVWkRAM5DxVKTcAAq1iP/m6hPSMwlN9+NA+d1fekvrHEF +xomnozUoWQ0lHeewDVNblSAvGGRZUZ2AI7PnQ2NPaZRA9b8/wdTl3NL/yfv/vN+8 +BIpY5Q== +-----END CERTIFICATE----- diff --git a/feature/system/attestz/tests/testdata/owner-ca.cert.pem b/feature/system/attestz/tests/testdata/owner-ca.cert.pem new file mode 100644 index 00000000000..db930dc2985 --- /dev/null +++ b/feature/system/attestz/tests/testdata/owner-ca.cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIUDG5yZigPNdiefkggL803LqhyxFgwDQYJKoZIhvcNAQEL +BQAwXTEQMA4GA1UEAwwHQ0EgMDAwMTELMAkGA1UEBhMCQVExCzAJBgNVBAgMAk5a +MQswCQYDVQQHDAJOWjEiMCAGA1UECgwZT3BlbkNvbmZpZ0ZlYXR1cmVQcm9maWxl +czAeFw0yNDA0MDExODUxNDNaFw0zNDAzMzAxODUxNDNaMF0xEDAOBgNVBAMMB0NB +IDAwMDExCzAJBgNVBAYTAkFRMQswCQYDVQQIDAJOWjELMAkGA1UEBwwCTloxIjAg +BgNVBAoMGU9wZW5Db25maWdGZWF0dXJlUHJvZmlsZXMwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC6V1P/Zj6s0yCiw3CY2mq79iUdVnj72xwZ3me9/oU+ +HZaOvmUoyTEg3KJGIIBZCTE4EZG0BFbjtkeuQWzYJviYUBphZyv7dURnNFyJcYWk +jLjk1aX3ynx0X+PGrLtECe/Fn3wfdjSg3qA39g0I+ZneeXeRMidXZFnvk/YJQnAn +UHTa9/raDAQoYtnNmzm+5meRo00BQR4rJuJJJg1+67fSSy3i6OQfFlC+eeGTyyjH +9hcAwrN48TVVv6IRcljyUX1GF0YpcdfpQXK6S7JCDSrwzTJeifcBN/2xPvqHb0fU +S4hIVtMy6KwUvaL7miKXxRJE6BFAQ1nhQCUQP+DBSNmnAgMBAAGjUzBRMB0GA1Ud +DgQWBBQzvHg3d3iZkDfSqtcX7xoSHisAyDAfBgNVHSMEGDAWgBQzvHg3d3iZkDfS +qtcX7xoSHisAyDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBu +VecPw0rw1gqnaH0M7qAjaayvo7AJyWhWSF3yFcsC/Wa/8xcaCQ2Rz+2g5di8E6n+ +fLb18rpFnt+vmUplz98ooqJeMYb6POCqGpu1dJU+utIzBYjYh4xld6UeR+KEnmuZ +IQF4ryLrBr9/CLgKv33AUw+XiTROkRf5+BaCgz/LgUENXSKtRJBMFSaNxpWnd0zy +WHveqEkyIV8XPFY186YjWiCW4hJ2M1yKhiotnOdUih7KXyOrv9lpzkLnSnW2wbtw +MH1kM7IfD2VAMO65o0wybF0Pzd+hALYGdO9A6QR9XwHZxyRKH4rPo3mVrzjeDubL +CPL/t7xS1FIL5Y+XTdkM +-----END CERTIFICATE----- diff --git a/feature/system/attestz/tests/testdata/owner-ca.key.pem b/feature/system/attestz/tests/testdata/owner-ca.key.pem new file mode 100644 index 00000000000..2f385e68f3b --- /dev/null +++ b/feature/system/attestz/tests/testdata/owner-ca.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6V1P/Zj6s0yCi +w3CY2mq79iUdVnj72xwZ3me9/oU+HZaOvmUoyTEg3KJGIIBZCTE4EZG0BFbjtkeu +QWzYJviYUBphZyv7dURnNFyJcYWkjLjk1aX3ynx0X+PGrLtECe/Fn3wfdjSg3qA3 +9g0I+ZneeXeRMidXZFnvk/YJQnAnUHTa9/raDAQoYtnNmzm+5meRo00BQR4rJuJJ +Jg1+67fSSy3i6OQfFlC+eeGTyyjH9hcAwrN48TVVv6IRcljyUX1GF0YpcdfpQXK6 +S7JCDSrwzTJeifcBN/2xPvqHb0fUS4hIVtMy6KwUvaL7miKXxRJE6BFAQ1nhQCUQ +P+DBSNmnAgMBAAECggEAAviCnyLtVgxBhJbGesWaEvVL4QEnGJ+jKu9aTmhNM4HX +1xq+54glIWxm0tW90HFWIgOGohRuOBPbhDmuF1/dU619mmmUMkfazCU9NOf7eZPB +dtwEaA+AWssUkMl4bLUlurz0hu1Kc58NsmH+bGIWy266q0Ps1zqKsTkbeu7IGzDK +zFJ/HoJvdNuNYdvaHH8ofvW9oDEiC2plfpNRMj13NLmXDZyXjY08654LRN5DogID +K6NjEEB7gyjzDcusu+2Y0S9YMtuDiJkhlyl1lpF9FKAvaGKP7Wn6M02Oksu6UlVO +kaMmAYighegY6JSGrtm+ppggGwoWgvfFCniAd8qicQKBgQDRvRCJnqPuo5CwWrEc +jalNS/YhJPfZJuFfJsWUsb2ynY/0We4ZtwLgpjBcxxPnExW/4RFEnZJ8Ro45e+bt +dpbVVQGXEz9GzwwqWnSJF0UOloSDRmmTmdKxaO3zaaRKbM9Oa3FkFC+hALN0g0Nz +l84encxgjuq3gPdlwFRzOBqp/QKBgQDjcR+WV/AQCtCEibON9YSilpbrKd8pjjzu +CnRcHPrRVLSmM4qElag8M3bJ0LFeaGcKtNC9F3A/u82BGuo91ZzXOdRmoJoZVV5I +vCRnDIKrYtLAE7HaJG2qV//bPm+n80PaTind7m3Sy5fcf0BLL3rOjsMcVJEgi+LR +YnODmh6BcwKBgQC8J9lzLE8yYagGnYWv8OIGBvRKLajvNTMPsm+kAoQEfddLxXWV +uhmpwU03nhybuwJS/a0JGjb0qDMlHKNBOpb70OO5TToB4vKt+DH9XlPET4GXZw6F +rIRYRaLaMFaDsfOUDU1PE9Dapg9Xof5b776otrVHlk64ysimjpD0QEujXQKBgQDj +U84elwZ7AlQoJPoyiZNobtupcNB82I5N3mUvLEgFsoRdGmb43hypD0dLsCuYEQHs +0Y1RcnvfN/bPc/dslnWNKWACs8NSTuFOEb7QwNBaPQwor4a0YnS6LfqtSFqRo7PO +HxH5oLZkWtoOqaG5hFta2ZZqWpwzy52Jar3Ka+DRwQKBgG8MRvHJzbGJiTzgn1O8 +1e4Xb8rJMVwki0LoWJPEUZkXvjjpNKp6OkEYpNEQADM0mPBwOX0hWFADglX/nMVT +s//bCl+S0jOwugVBfDfJIJf9n+QhEI3mdKxywx2vCNklEDEcvnUuSIeWrY5/E8af +z8t9XRIHIeixrlNbTWFQGVv8 +-----END PRIVATE KEY----- diff --git a/go.mod b/go.mod index c93984c9271..cc39b95a8e4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/openconfig/featureprofiles -go 1.21 +go 1.22 require ( cloud.google.com/go/pubsub v1.36.1 @@ -11,11 +11,13 @@ require ( github.com/golang/glog v1.2.1 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v50 v50.1.0 + github.com/google/go-tpm v0.9.1 github.com/google/gopacket v1.1.19 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/kr/pretty v0.3.1 github.com/open-traffic-generator/snappi/gosnappi v1.3.0 + github.com/openconfig/attestz v0.2.0 github.com/openconfig/containerz v0.0.0-20240620162940-e0bf23af17d6 github.com/openconfig/entity-naming v0.0.0-20230912181021-7ac806551a31 github.com/openconfig/gnmi v0.11.0 @@ -120,7 +122,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/networkop/meshnet-cni v0.3.1-0.20230525201116-d7c306c635cf // indirect github.com/open-traffic-generator/keng-operator v0.3.28 // indirect - github.com/openconfig/attestz v0.2.0 // indirect github.com/openconfig/gnpsi v0.3.2 // indirect github.com/openconfig/grpctunnel v0.0.0-20220819142823-6f5422b8ca70 // indirect github.com/openconfig/lemming/operator v0.2.0 // indirect diff --git a/go.sum b/go.sum index 454fb06c5bc..b1f25ad48b7 100644 --- a/go.sum +++ b/go.sum @@ -1467,6 +1467,8 @@ github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMc github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/internal/security/attestz/attestz.go b/internal/security/attestz/attestz.go new file mode 100644 index 00000000000..5745d47e527 --- /dev/null +++ b/internal/security/attestz/attestz.go @@ -0,0 +1,517 @@ +// 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 attestz + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-tpm/tpm2" + cdpb "github.com/openconfig/attestz/proto/common_definitions" + attestzpb "github.com/openconfig/attestz/proto/tpm_attestz" + enrollzpb "github.com/openconfig/attestz/proto/tpm_enrollz" + "github.com/openconfig/featureprofiles/internal/components" + "github.com/openconfig/featureprofiles/internal/security/svid" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "golang.org/x/exp/slices" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/protobuf/testing/protocmp" +) + +// ControlCard struct to hold certificates for a control card. +type ControlCard struct { + Role cdpb.ControlCardRole + Name string + IAKCert string + IDevIDCert string + OIAKCert string + ODevIDCert string + MtlsCert string +} + +// AttestzSession represents grpc session used for attestz rpcs. +type AttestzSession struct { + Conn *grpc.ClientConn + Peer *peer.Peer + EnrollzClient enrollzpb.TpmEnrollzServiceClient + AttestzClient attestzpb.TpmAttestzServiceClient +} + +var ( + chassisName string + activeCard *ControlCard + standbyCard *ControlCard + pcrBankHashAlgo = []attestzpb.Tpm20HashAlgo{ + attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA1, + attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA256, + attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA384, + attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA512, + } + PcrBankHashAlgoMap = map[ondatra.Vendor][]attestzpb.Tpm20HashAlgo{ + ondatra.NOKIA: {attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA1, attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA256}, + ondatra.ARISTA: pcrBankHashAlgo, + ondatra.JUNIPER: pcrBankHashAlgo, + ondatra.CISCO: pcrBankHashAlgo, + } + PcrIndices = []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 23} +) + +// PrettyPrint prints any type in a pretty format. +func PrettyPrint(i interface{}) string { + s, _ := json.MarshalIndent(i, "", "\t") + return string(s) +} + +// EnrollzWorkflow performs enrollment workflow for a given control card. +func (cc *ControlCard) EnrollzWorkflow(t *testing.T, dut *ondatra.DUTDevice, tc *TlsConf, vendorCaCertFile string) { + var as *AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + + // Get vendor certs. + resp := as.GetVendorCerts(t, ParseRoleSelection(cc.Role)) + cc.IAKCert, cc.IDevIDCert = resp.IakCert, resp.IdevidCert + + // Verify active card's cert is used for tls connection. + t.Logf("Verifying correct cert was used for tls connection during enrollz.") + var activeCert string + if activeCard.MtlsCert != "" { + activeCert = activeCard.MtlsCert + } else if activeCard.ODevIDCert != "" { + activeCert = activeCard.ODevIDCert + } else { + activeCert = activeCard.IDevIDCert + } + wantPeerCert, err := LoadCertificate(activeCert) + if err != nil { + t.Fatalf("Error parsing cert. error: %s", err) + } + tlsInfo := as.Peer.AuthInfo.(credentials.TLSInfo) + gotPeerCert := tlsInfo.State.PeerCertificates[0] + if diff := cmp.Diff(wantPeerCert, gotPeerCert); diff != "" { + t.Errorf("Incorrect certificate used for enrollz tls session. -want,+got:\n%s", diff) + } + + // Load vendor ca certificate. + vendorCaPem, err := os.ReadFile(vendorCaCertFile) + if err != nil { + t.Fatalf("Error reading vendor cert. error: %v", err) + } + + // Verify cert info. + t.Logf("Verifying IDevID cert for card %v", cc.Name) + cc.verifyVendorCert(t, dut, vendorCaPem, "idevid") + t.Logf("Verifying IAK cert for card %v", cc.Name) + cc.verifyVendorCert(t, dut, vendorCaPem, "iak") + t.Logf("Verifying control card details for card %v", cc.Name) + cc.verifyControlCardInfo(t, dut, resp.ControlCardId) + + // Generate owner certificates. + caKey, caCert, err := svid.LoadKeyPair(tc.CaKeyFile, tc.CaCertFile) + if err != nil { + t.Fatalf("Could not load ca key/cert. error: %v", err) + } + t.Logf("Generating oIAK cert for card %v", cc.Name) + cc.OIAKCert = GenOwnerCert(t, caKey, caCert, cc.IAKCert, nil, tc.Target) + t.Logf("Generating oDevID cert for card %v", cc.Name) + cc.ODevIDCert = GenOwnerCert(t, caKey, caCert, cc.IDevIDCert, nil, tc.Target) + + // Rotate owner certificates. + as.RotateOwnerCerts(t, cc.Role, cc.OIAKCert, cc.ODevIDCert, sslProfileId) +} + +// GenNonce creates a random 32 byte nonce used for attest rpc. +func GenNonce(t *testing.T) []byte { + nonce := make([]byte, 32) + _, err := rand.Read(nonce) + if err != nil { + t.Fatalf("Error generating nonce. error: %v", err) + } + return nonce +} + +// AttestzWorkflow performs attestation workflow for a given control card. +func (cc *ControlCard) AttestzWorkflow(t *testing.T, dut *ondatra.DUTDevice, tc *TlsConf) { + var as *AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + + for _, hashAlgo := range PcrBankHashAlgoMap[dut.Vendor()] { + nonce := GenNonce(t) + attestResponse := as.RequestAttestation(t, cc.Role, nonce, hashAlgo, PcrIndices) + + // Verify active card's cert is used for tls connection. + t.Logf("Verifying correct cert was used for tls connection during attestz") + var activeCert string + if activeCard.MtlsCert != "" { + activeCert = activeCard.MtlsCert + } else { + activeCert = activeCard.ODevIDCert + } + wantPeerCert, err := LoadCertificate(activeCert) + if err != nil { + t.Fatalf("Error parsing cert. error: %s", err) + } + tlsInfo := as.Peer.AuthInfo.(credentials.TLSInfo) + gotPeerCert := tlsInfo.State.PeerCertificates[0] + if diff := cmp.Diff(wantPeerCert, gotPeerCert); diff != "" { + t.Errorf("Incorrect certificate used for attestz tls session. -want,+got:\n%s", diff) + } + t.Logf("Verifying attestation for card %v, hash algo: %v", cc.Name, hashAlgo.String()) + + cc.verifyAttestation(t, dut, attestResponse, nonce, hashAlgo, PcrIndices) + } +} + +// ParseRoleSelection returns crafted get request with card role. +func ParseRoleSelection(inputRole cdpb.ControlCardRole) *cdpb.ControlCardSelection { + return &cdpb.ControlCardSelection{ + ControlCardId: &cdpb.ControlCardSelection_Role{ + Role: inputRole, + }, + } +} + +// ParseSerialSelection returns crafted get request with card serial. +func ParseSerialSelection(inputSerial string) *cdpb.ControlCardSelection { + return &cdpb.ControlCardSelection{ + ControlCardId: &cdpb.ControlCardSelection_Serial{ + Serial: inputSerial, + }, + } +} + +// GetVendorCerts returns vendor certs from the dut for a given card. +func (as *AttestzSession) GetVendorCerts(t *testing.T, cardSelection *cdpb.ControlCardSelection) *enrollzpb.GetIakCertResponse { + enrollzRequest := &enrollzpb.GetIakCertRequest{ + ControlCardSelection: cardSelection, + } + t.Logf("Sending Enrollz.GetIakCert request on device: \n %s", PrettyPrint(enrollzRequest)) + response, err := as.EnrollzClient.GetIakCert(context.Background(), enrollzRequest, grpc.Peer(as.Peer)) + if err != nil { + t.Fatalf("Error getting vendor certs. error: %v", err) + } + t.Logf("GetIakCert response: \n %s", PrettyPrint(response)) + return response +} + +// RotateOwnerCerts pushes owner certs to the dut for a given card & ssl profile. +func (as *AttestzSession) RotateOwnerCerts(t *testing.T, cardRole cdpb.ControlCardRole, oIAKCert string, oDevIDCert string, sslProfileId string) { + enrollzRequest := &enrollzpb.RotateOIakCertRequest{ + ControlCardSelection: ParseRoleSelection(cardRole), + OiakCert: oIAKCert, + OidevidCert: oDevIDCert, + SslProfileId: sslProfileId, + } + t.Logf("Sending Enrollz.Rotate request on device: \n %s", PrettyPrint(enrollzRequest)) + _, err := as.EnrollzClient.RotateOIakCert(context.Background(), enrollzRequest, grpc.Peer(as.Peer)) + if err != nil { + t.Fatalf("Error with RotateOIakCert. error: %v", err) + } + // Brief sleep for rotate to get processed. + time.Sleep(time.Second) +} + +// RequestAttestation requests attestation from the dut for a given card, hash algo & pcr indices. +func (as *AttestzSession) RequestAttestation(t *testing.T, cardRole cdpb.ControlCardRole, nonce []byte, hashAlgo attestzpb.Tpm20HashAlgo, pcrIndices []int32) *attestzpb.AttestResponse { + attestzRequest := &attestzpb.AttestRequest{ + ControlCardSelection: ParseRoleSelection(cardRole), + Nonce: nonce, + HashAlgo: hashAlgo, + PcrIndices: pcrIndices, + } + t.Logf("Sending Attestz.Attest request on device: \n %s", PrettyPrint(attestzRequest)) + response, err := as.AttestzClient.Attest(context.Background(), attestzRequest, grpc.Peer(as.Peer)) + if err != nil { + t.Fatalf("Error with AttestRequest. error: %v", err) + } + t.Logf("Attest response: \n %s", PrettyPrint(response)) + return response +} + +func (cc *ControlCard) verifyVendorCert(t *testing.T, dut *ondatra.DUTDevice, vendorCaCert []byte, certType string) { + vendorCa, err := LoadCertificate(string(vendorCaCert)) + if err != nil { + t.Fatalf("Error loading vendor ca certificate. error: %v", err) + } + + var cert string + switch certType { + case "idevid": + cert = cc.IDevIDCert + case "iak": + cert = cc.IAKCert + } + + vendorCert, err := LoadCertificate(cert) + if err != nil { + t.Fatalf("Error loading vendor certificate. error: %v", err) + } + + // Formatting time to RFC3339 to ensure ease for comparing. + expectedTime, _ := time.Parse(time.RFC3339, "9999-12-31T23:59:59Z") + if !expectedTime.Equal(vendorCert.NotAfter) { + t.Fatalf("Did not get expected NotAfter date, got: %v, want: %v", vendorCert.NotAfter, expectedTime) + } + + // Ensure that NotBefore is in the past (should be creation date of the cert, which should always be in the past). + currentTime := time.Now() + if currentTime.Before(vendorCert.NotBefore) { + t.Fatalf("Did not get expected NotBefore date, got: %v, want: earlier than %v", vendorCert.NotBefore, currentTime) + } + + // Verify cert matches the serial number of the card queried. + serialNo := gnmi.Get[string](t, dut, gnmi.OC().Component(cc.Name).SerialNo().State()) + if vendorCert.Subject.SerialNumber != serialNo { + t.Fatalf("Got wrong serial number, got: %v, want: %v", vendorCert.Subject.SerialNumber, serialNo) + } + + if !strings.EqualFold(vendorCert.Subject.Organization[0], dut.Vendor().String()) { + t.Fatalf("Wrong signature on Sub Org. got: %v, want: %v", strings.ToLower(vendorCert.Subject.Organization[0]), strings.ToLower(dut.Vendor().String())) + } + + // Verify cert is signed by switch vendor ca. + switch vendorCert.SignatureAlgorithm { + case x509.SHA384WithRSA: + // Generate Hash from Raw Certificate + certHash := generateHash(vendorCert.RawTBSCertificate, crypto.SHA384) + // Retrieve CA Public Key + vendorCaPubKey := vendorCa.PublicKey.(*rsa.PublicKey) + // Verify digital signature with oIAK cert. + err = rsa.VerifyPKCS1v15(vendorCaPubKey, crypto.SHA384, certHash, vendorCert.Signature) + if err != nil { + t.Fatalf("Failed verifying vendor cert's signature: %v", err) + } + default: + t.Errorf("Cannot verify signature for %v for cert: %s", vendorCert.SignatureAlgorithm, PrettyPrint(vendorCert)) + } +} + +func (cc *ControlCard) verifyControlCardInfo(t *testing.T, dut *ondatra.DUTDevice, gotCardDetails *cdpb.ControlCardVendorId) { + controller := gnmi.Get[*oc.Component](t, dut, gnmi.OC().Component(cc.Name).State()) + if chassisName == "" { + chassisName = components.FindComponentsByType(t, dut, oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_CHASSIS)[0] + } + chassis := gnmi.Get[*oc.Component](t, dut, gnmi.OC().Component(chassisName).State()) + wantCardDetails := &cdpb.ControlCardVendorId{ + ControlCardRole: cc.Role, + ControlCardSerial: controller.GetSerialNo(), + ControlCardSlot: string(controller.GetLocation()[len(controller.GetLocation())-1]), + ChassisManufacturer: chassis.GetMfgName(), + ChassisSerialNumber: chassis.GetSerialNo(), + ChassisPartNumber: chassis.GetPartNo(), + } + + if diff := cmp.Diff(wantCardDetails, gotCardDetails, protocmp.Transform()); diff != "" { + t.Errorf("Got diff in vendor card details: -want,+got:\n%s", diff) + } +} + +func generateHash(quote []byte, hashAlgo crypto.Hash) []byte { + switch hashAlgo { + case crypto.SHA1: + quoteHash := sha1.Sum(quote) + return quoteHash[:] + case crypto.SHA256: + quoteHash := sha256.Sum256(quote) + return quoteHash[:] + case crypto.SHA384: + quoteHash := sha512.Sum384(quote) + return quoteHash[:] + case crypto.SHA512: + quoteHash := sha512.Sum512(quote) + return quoteHash[:] + } + return nil +} + +// Verify Nokia pcr with expected values. Ensure secure-boot is enabled for pcr values to match. +func nokiaPCRVerify(t *testing.T, dut *ondatra.DUTDevice, cardName string, hashAlgo attestzpb.Tpm20HashAlgo, gotPcrValues map[int32][]byte) error { + ver := gnmi.Get[string](t, dut, gnmi.OC().System().SoftwareVersion().State()) + t.Logf("Found software version: %v", ver) + + // Expected pcr values for Nokia present in /mnt/nokiaos//known_good_pcr_values.json. + sshC, err := dut.RawAPIs().BindingDUT().DialCLI(context.Background()) + if err != nil { + t.Logf("Could not connect ssh. error: %v", err) + } + cmd := fmt.Sprintf("cat /mnt/nokiaos/%s/known_good_pcr_values.json", ver) + res, err := sshC.RunCommand(context.Background(), cmd) + if err != nil { + t.Fatalf("Could not run command: %v, error: %v", cmd, err) + } + + // Parse json file into struct. + type PcrValuesData struct { + Pcr int32 `json:"pcr"` + Value string `json:"value"` + } + type PcrBankData struct { + Bank string `json:"bank"` + Values []PcrValuesData `json:"values"` + } + type CardData struct { + Card string `json:"card"` + Pcrs []PcrBankData `json:"pcrs"` + } + type PcrData struct { + Cards []CardData `json:"cards"` + } + var nokiaPcrData PcrData + err = json.Unmarshal([]byte(res.Output()), &nokiaPcrData) + if err != nil { + t.Fatalf("Could not parse json. error: %v", err) + } + + hashAlgoMap := map[attestzpb.Tpm20HashAlgo]string{ + attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA1: "sha1", + attestzpb.Tpm20HashAlgo_TPM20HASH_ALGO_SHA256: "sha256", + } + + // Verify pcr_values match expectations. + pcrIndices := []int32{0, 2, 4, 6, 9, 14} + cardDesc := gnmi.Get[string](t, dut, gnmi.OC().Component(cardName).Description().State()) + idx := slices.IndexFunc(nokiaPcrData.Cards, func(c CardData) bool { + return c.Card == cardDesc + }) + if idx == -1 { + return fmt.Errorf("Could not find card %v in reference data.", cardDesc) + } + + pcrBankData := nokiaPcrData.Cards[idx].Pcrs + idx = slices.IndexFunc(pcrBankData, func(p PcrBankData) bool { + return p.Bank == hashAlgoMap[hashAlgo] + }) + if idx == -1 { + return fmt.Errorf("Could not find pcr bank %v in reference data.", hashAlgoMap[hashAlgo]) + } + + wantPcrValues := pcrBankData[idx].Values + for _, pcrIndex := range pcrIndices { + idx = slices.IndexFunc(wantPcrValues, func(p PcrValuesData) bool { + return p.Pcr == pcrIndex + }) + if idx == -1 { + return fmt.Errorf("Could not find pcr index %v in reference data.", pcrIndex) + } + if got, want := hex.EncodeToString(gotPcrValues[pcrIndex]), wantPcrValues[idx].Value; got != want { + t.Errorf("%v pcr %v value does not match expectations, got: %v want: %v", hashAlgoMap[hashAlgo], pcrIndex, got, want) + } + } + return nil +} + +func (cc *ControlCard) verifyAttestation(t *testing.T, dut *ondatra.DUTDevice, attestResponse *attestzpb.AttestResponse, wantNonce []byte, pcrHashAlgo attestzpb.Tpm20HashAlgo, pcrIndices []int32) { + // Verify oIAK cert is the same as the one installed earlier. + if !cmp.Equal(attestResponse.OiakCert, cc.OIAKCert) { + t.Errorf("Got incorrect oIAK cert, got: %v, want: %v", attestResponse.OiakCert, cc.OIAKCert) + } + + // Verify all pcr_values match expectations. + switch dut.Vendor() { + case ondatra.NOKIA: + if err := nokiaPCRVerify(t, dut, cc.Name, pcrHashAlgo, attestResponse.PcrValues); err != nil { + t.Error(err) + } + default: + t.Error("Vendor reference pcr values not verified.") + } + + // Retrieve quote signature in TPM Object + quoteTpmtSignature, err := tpm2.Unmarshal[tpm2.TPMTSignature](attestResponse.QuoteSignature) + if err != nil { + t.Fatalf("Error unmarshalling signature. error: %v", err) + } + + // Default Hash Algo is SHA256 as per TPM2_Quote(). + // https://github.com/tpm2-software/tpm2-tools/blob/master/man/tpm2_quote.1.md + hashAlgo := crypto.SHA256 + + oIakCert, err := LoadCertificate(cc.OIAKCert) + if err != nil { + t.Fatalf("Error loading vendor oIAK cert. error: %v", err) + } + + switch quoteTpmtSignature.SigAlg { + case tpm2.TPMAlgRSASSA: + quoteTpmsSignature, err := quoteTpmtSignature.Signature.RSASSA() + if err != nil { + t.Fatalf("Error retrieving TPMS signature. error: %v", err) + } + // Retrieve signature's hash algorithm. + hashAlgo, err = quoteTpmsSignature.Hash.Hash() + if err != nil { + t.Fatalf("Error retrieving signature hash algorithm. error: %v", err) + } + // Generate hash from original quote + quoteHash := generateHash(attestResponse.Quoted, hashAlgo) + // Retrieve oIAK public key. + oIAKPubKey := oIakCert.PublicKey.(*rsa.PublicKey) + // Verify quote signature with oIAK cert. + err = rsa.VerifyPKCS1v15(oIAKPubKey, hashAlgo, quoteHash, quoteTpmsSignature.Sig.Buffer) + if err != nil { + t.Fatalf("Failed verifying quote signature. error: %v", err) + } + default: + t.Errorf("Cannot verify signature for %v. quote signature: %s", quoteTpmtSignature.SigAlg, PrettyPrint(quoteTpmtSignature)) + } + + // Concatenate pcr values & generate pcr digest. + var concatPcrs []byte + for _, idx := range pcrIndices { + concatPcrs = append(concatPcrs, attestResponse.PcrValues[idx]...) + } + wantPcrDigest := generateHash(concatPcrs, hashAlgo) + + // Retrieve pcr digest from quote. + quoted, err := tpm2.Unmarshal[tpm2.TPMSAttest](attestResponse.Quoted) + if err != nil { + t.Fatalf("Error unmarshalling quote. error: %v", err) + } + tpmsQuoteInfo, err := quoted.Attested.Quote() + if err != nil { + t.Fatalf("Error getting TPMS quote info. error: %v", err) + } + gotPcrDigest := tpmsQuoteInfo.PCRDigest.Buffer + + // Verify recomputed PCR digest matches with pcr digest in quote. + if !cmp.Equal(gotPcrDigest, wantPcrDigest) { + t.Fatalf("Did not receive expected pcr digest from attest rpc, got: %v, want: %v", gotPcrDigest, wantPcrDigest) + } + + // Verify nonce. + gotNonce := quoted.ExtraData.Buffer + if !cmp.Equal(gotNonce, wantNonce) { + t.Logf("Did not receive expected nonce, got: %v, want: %v", gotNonce, wantNonce) + } +} diff --git a/internal/security/attestz/cert.go b/internal/security/attestz/cert.go new file mode 100644 index 00000000000..a9da3ed989e --- /dev/null +++ b/internal/security/attestz/cert.go @@ -0,0 +1,340 @@ +// 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 attestz + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "testing" + "time" + + certzpb "github.com/openconfig/gnsi/certz" + "github.com/openconfig/ondatra" +) + +type CertType int + +const ( + CertTypeRaw CertType = 0 + CertTypeIdevid CertType = 1 +) + +type entityType int + +const ( + entityTypeCertificate entityType = 0 + entityTypeTrust entityType = 1 +) + +// AddProfile adds ssl profile on the dut. +func AddProfile(t *testing.T, dut *ondatra.DUTDevice, sslProfileId string) { + t.Logf("Performing Certz.AddProfile on device %s for profile %s", dut.Name(), sslProfileId) + gnsiC, err := dut.RawAPIs().BindingDUT().DialGNSI(context.Background()) + if err != nil { + t.Errorf("gNSI client error: %v", err) + } + _, err = gnsiC.Certz().AddProfile(context.Background(), &certzpb.AddProfileRequest{ + SslProfileId: sslProfileId, + }) + if err != nil { + t.Fatalf("Error adding tls profile via certz. error: %v", err) + } +} + +// DeleteProfile delete ssl profile from the dut. +func DeleteProfile(t *testing.T, dut *ondatra.DUTDevice, sslProfileId string) { + t.Logf("Performing Certz.DeleteProfile on device %s for profile %s", dut.Name(), sslProfileId) + gnsiC, err := dut.RawAPIs().BindingDUT().DialGNSI(context.Background()) + if err != nil { + t.Errorf("gNSI client error: %v", err) + } + _, err = gnsiC.Certz().DeleteProfile(context.Background(), &certzpb.DeleteProfileRequest{ + SslProfileId: sslProfileId, + }) + if err != nil { + t.Fatalf("Error deleting tls profile via certz. error: %v", err) + } +} + +func createEntity(entityType entityType, certificate *certzpb.Certificate) *certzpb.Entity { + entity := &certzpb.Entity{ + Version: fmt.Sprintf("v0.%v", time.Now().Unix()), + CreatedOn: uint64(time.Now().Unix()), + } + certChain := &certzpb.CertificateChain{ + Certificate: certificate, + } + + switch entityType { + case entityTypeCertificate: + entity.Entity = &certzpb.Entity_CertificateChain{ + CertificateChain: certChain, + } + case entityTypeTrust: + entity.Entity = &certzpb.Entity_TrustBundle{ + TrustBundle: certChain, + } + } + + return entity +} + +func createCertificate(rotateType CertType, keyContents, certContents []byte) *certzpb.Certificate { + cert := &certzpb.Certificate{ + Type: certzpb.CertificateType_CERTIFICATE_TYPE_X509, + Encoding: certzpb.CertificateEncoding_CERTIFICATE_ENCODING_PEM, + } + + switch rotateType { + case CertTypeIdevid: + cert.PrivateKeyType = &certzpb.Certificate_KeySource_{ + KeySource: certzpb.Certificate_KEY_SOURCE_IDEVID_TPM, + } + cert.CertificateType = &certzpb.Certificate_CertSource_{ + CertSource: certzpb.Certificate_CERT_SOURCE_IDEVID, + } + case CertTypeRaw: + cert.PrivateKeyType = &certzpb.Certificate_RawPrivateKey{ + RawPrivateKey: keyContents, + } + cert.CertificateType = &certzpb.Certificate_RawCertificate{ + RawCertificate: certContents, + } + } + + return cert +} + +// RotateCerts rotates certificates on the dut for a given ssl profile. +func RotateCerts(t *testing.T, dut *ondatra.DUTDevice, rotateType CertType, sslProfileId string, dutKey, dutCert, trustBundle []byte) { + t.Logf("Performing Certz.Rotate request on device %s", dut.Name()) + gnsiC, err := dut.RawAPIs().BindingDUT().DialGNSI(context.Background()) + if err != nil { + t.Errorf("gNSI client error: %v", err) + } + rotateStream, err := gnsiC.Certz().Rotate(context.Background()) + if err != nil { + t.Fatalf("Could not start a rotate stream. error: %v", err) + } + defer rotateStream.CloseSend() + + var entities []*certzpb.Entity + switch rotateType { + case CertTypeIdevid: + certificate := createCertificate(rotateType, nil, nil) + entities = append(entities, createEntity(entityTypeCertificate, certificate)) + case CertTypeRaw: + if dutKey != nil && dutCert != nil { + certificate := createCertificate(rotateType, dutKey, dutCert) + entities = append(entities, createEntity(entityTypeCertificate, certificate)) + } + if trustBundle != nil { + certificate := createCertificate(rotateType, nil, trustBundle) + entities = append(entities, createEntity(entityTypeTrust, certificate)) + } + } + + // Create rotate request. + certzRequest := &certzpb.RotateCertificateRequest{ + ForceOverwrite: true, + SslProfileId: sslProfileId, + RotateRequest: &certzpb.RotateCertificateRequest_Certificates{ + Certificates: &certzpb.UploadRequest{ + Entities: entities, + }, + }, + } + + // Send rotate request. + t.Logf("Sending Certz.Rotate request on device: \n %s", PrettyPrint(certzRequest)) + err = rotateStream.Send(certzRequest) + if err != nil { + t.Fatalf("Error while uploading certz request. error: %v", err) + } + t.Logf("Certz.Rotate upload was successful, receiving response ...") + _, err = rotateStream.Recv() + if err != nil { + t.Fatalf("Error while receiving certz rotate reply. error: %v", err) + } + + // Finalize rotation. + finalizeRotateRequest := &certzpb.RotateCertificateRequest{ + SslProfileId: sslProfileId, + RotateRequest: &certzpb.RotateCertificateRequest_FinalizeRotation{ + FinalizeRotation: &certzpb.FinalizeRequest{}, + }, + } + t.Logf("Sending Certz.Rotate FinalizeRotation request: \n %s", PrettyPrint(finalizeRotateRequest)) + err = rotateStream.Send(finalizeRotateRequest) + if err != nil { + t.Fatalf("Error while finalizing rotate request. error: %v", err) + } + + // Brief sleep for finalize to get processed. + time.Sleep(time.Second) +} + +// LoadCertificate decodes certificate provided as a string. +func LoadCertificate(cert string) (*x509.Certificate, error) { + certPem, _ := pem.Decode([]byte(cert)) + if certPem == nil { + return nil, fmt.Errorf("Error decoding certificate.") + } + return x509.ParseCertificate(certPem.Bytes) +} + +// GenOwnerCert creates switch owner iak/idevid certs & signs it based on given ca cert/key. +func GenOwnerCert(t *testing.T, caKey any, caCert *x509.Certificate, inputCert string, pubKey any, netAddr string) string { + cert, err := LoadCertificate(inputCert) + if err != nil { + t.Fatalf("Error loading vendor certificate. error: %v", err) + } + if pubKey == nil { + pubKey = cert.PublicKey + } + + // Generate Random Serial Number as per TCG Spec (between 64 and 160 bits). + // https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf#page=55 + minBits := 64 + maxBits := 160 + minVal := new(big.Int).Lsh(big.NewInt(1), uint(minBits-1)) // minVal = 2^63 + maxVal := new(big.Int).Lsh(big.NewInt(1), uint(maxBits)) // maxVal = 2^160 + // Random number between [2^63, 2^160) + serial, err := rand.Int(rand.Reader, maxVal.Sub(maxVal, minVal)) + if err != nil { + t.Fatalf("Error generating serial number. error: %s", err) + } + serial.Add(minVal, serial) + t.Logf("Generated new serial number for cert: %s", serial) + + // Get IP Address from gnsi target + ip, _, err := net.SplitHostPort(netAddr) + if err != nil { + t.Errorf("Error parsing host:port info. error: %v", err) + } + + // Create switch owner certificate template. + ownerCert := &x509.Certificate{ + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: cert.NotAfter, + Subject: cert.Subject, + KeyUsage: cert.KeyUsage, + ExtKeyUsage: cert.ExtKeyUsage, + IPAddresses: []net.IP{net.ParseIP(ip)}, + } + + // Sign certificate with switch owner ca. + certBytes, err := x509.CreateCertificate(rand.Reader, ownerCert, caCert, pubKey, caKey) + if err != nil { + t.Fatalf("Could not generate owner certificate. error: %v", err) + } + + // PEM encode switch owner certificate. + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + certPEM := new(bytes.Buffer) + if err := pem.Encode(certPEM, pemBlock); err != nil { + t.Fatalf("Could not PEM encode owner certificate. error: %v", err) + } + + return certPEM.String() +} + +// GenTlsCert creates mtls client/server certificates & signs it based on given signing cert/key. +func GenTlsCert(ip string, signingCert *x509.Certificate, signingKey any, keyAlgo x509.PublicKeyAlgorithm) ([]byte, []byte, error) { + certSpec, err := populateCertTemplate(ip) + if err != nil { + return nil, nil, err + } + var privKey crypto.PrivateKey + switch keyAlgo { + case x509.RSA: + privKey, err = rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, err + } + case x509.ECDSA: + curve := elliptic.P256() + privKey, err = ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, nil, err + } + default: + return nil, nil, fmt.Errorf("Key algorithm %v is not supported.", keyAlgo) + } + pubKey := privKey.(crypto.Signer).Public() + certBytes, err := x509.CreateCertificate(rand.Reader, certSpec, signingCert, pubKey, signingKey) + if err != nil { + return nil, nil, err + } + // PEM encode certificate. + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + certPem := new(bytes.Buffer) + if err = pem.Encode(certPem, pemBlock); err != nil { + return nil, nil, err + } + privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + return nil, nil, err + } + // PEM encode private key. + pemBlock = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: privKeyBytes, + } + privKeyPem := new(bytes.Buffer) + if err = pem.Encode(privKeyPem, pemBlock); err != nil { + return nil, nil, err + } + return certPem.Bytes(), privKeyPem.Bytes(), nil +} + +func populateCertTemplate(ip string) (*x509.Certificate, error) { + serial, err := rand.Int(rand.Reader, big.NewInt(big.MaxBase)) + if err != nil { + return nil, err + } + certSpec := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: ip, + Organization: []string{"OpenconfigFeatureProfiles"}, + Country: []string{"US"}, + }, + IPAddresses: []net.IP{net.ParseIP(ip)}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(0, 0, 1), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + return certSpec, nil +} diff --git a/internal/security/attestz/events.go b/internal/security/attestz/events.go new file mode 100644 index 00000000000..f34dec40d3f --- /dev/null +++ b/internal/security/attestz/events.go @@ -0,0 +1,151 @@ +// 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 attestz + +import ( + "context" + "testing" + "time" + + "github.com/openconfig/featureprofiles/internal/components" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + frpb "github.com/openconfig/gnoi/factory_reset" + gnps "github.com/openconfig/gnoi/system" + "github.com/openconfig/gnoigo/system" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ondatra/gnoi" + "github.com/openconfig/testt" + "github.com/openconfig/ygnmi/ygnmi" +) + +const ( + maxRebootTime = 15 // Unit is minutes + maxFactoryResetRebootTime = 40 // Unit is minutes, includes time for bootz + maxSwitchoverTime = 900 // Unit is seconds +) + +// SwitchoverReady waits for control cards to become switchover ready. +func SwitchoverReady(t *testing.T, dut *ondatra.DUTDevice, activeCard, standbyCard string) { + switchoverReady := gnmi.OC().Component(activeCard).SwitchoverReady() + _, ok := gnmi.Watch(t, dut, switchoverReady.State(), 30*time.Minute, func(val *ygnmi.Value[bool]) bool { + ready, present := val.Val() + return present && ready + }).Await(t) + if !ok { + activeCardPath := gnmi.OC().Component(activeCard).State() + standbyCardPath := gnmi.OC().Component(standbyCard).State() + fptest.LogQuery(t, "Active card reported state", activeCardPath, gnmi.Get[*oc.Component](t, dut, activeCardPath)) + fptest.LogQuery(t, "Standby card reported state", standbyCardPath, gnmi.Get[*oc.Component](t, dut, standbyCardPath)) + t.Fatal("Cards are not synchronized.") + } +} + +func waitForBootup(t *testing.T, dut *ondatra.DUTDevice, maxBootTime uint64) { + startTime := time.Now() + for { + if errMsg := testt.CaptureFatal(t, func(t testing.TB) { + gnmi.Get[string](t, dut, gnmi.OC().System().CurrentDatetime().State()) + }); errMsg != nil { + t.Log("Reboot is started ...") + break + } + t.Log("Wait for reboot ...") + time.Sleep(30 * time.Second) + } + t.Logf("Wait for DUT to boot up by polling the telemetry output ...") + for { + var currentTime string + t.Logf("Time elapsed %.2f minutes since reboot started.", time.Since(startTime).Minutes()) + if errMsg := testt.CaptureFatal(t, func(t testing.TB) { + currentTime = gnmi.Get[string](t, dut, gnmi.OC().System().CurrentDatetime().State()) + }); errMsg != nil { + t.Logf("Got testt.CaptureFatal errMsg: %s, keep polling ...", *errMsg) + } else { + t.Logf("Device rebooted successfully with received time: %v", currentTime) + break + } + if uint64(time.Since(startTime).Minutes()) > maxBootTime { + t.Fatalf("Check boot time: got %v, want < %v", time.Since(startTime), maxBootTime) + } + time.Sleep(30 * time.Second) + } + t.Logf("Device boot time: %.2f minutes", time.Since(startTime).Minutes()) +} + +// RebootDut reboots the dut. +func RebootDut(t *testing.T, dut *ondatra.DUTDevice) { + gnoiClient, err := dut.RawAPIs().BindingDUT().DialGNOI(context.Background()) + if err != nil { + t.Fatalf("Failed to connect to gNOI server, err: %v", err) + } + rebootRequest := &gnps.RebootRequest{ + Method: gnps.RebootMethod_COLD, + Force: true, + } + bootTimeBeforeReboot := gnmi.Get[uint64](t, dut, gnmi.OC().System().BootTime().State()) + t.Logf("DUT boot time before reboot: %v", time.Unix(0, int64(bootTimeBeforeReboot))) + currentTime := gnmi.Get[string](t, dut, gnmi.OC().System().CurrentDatetime().State()) + t.Logf("DUT system time before reboot : %s", currentTime) + res, err := gnoiClient.System().Reboot(context.Background(), rebootRequest) + if err != nil { + t.Fatalf("Failed to reboot chassis with unexpected error: %v", err) + } + t.Logf("Reboot Response %v ", PrettyPrint(res)) + waitForBootup(t, dut, maxRebootTime) +} + +// FactoryResetDut factory resets the dut. +func FactoryResetDut(t *testing.T, dut *ondatra.DUTDevice) { + gnoiClient, err := dut.RawAPIs().BindingDUT().DialGNOI(context.Background()) + if err != nil { + t.Fatalf("Error dialing gNOI: %v", err) + } + res, err := gnoiClient.FactoryReset().Start(context.Background(), &frpb.StartRequest{FactoryOs: false, ZeroFill: false}) + if err != nil { + t.Fatalf("Failed to initiate factory reset on the device. error : %v ", err) + } + t.Logf("Factory reset response: %v ", PrettyPrint(res)) + waitForBootup(t, dut, maxFactoryResetRebootTime) +} + +// SwitchoverCards performs control card switchover. +func SwitchoverCards(t *testing.T, dut *ondatra.DUTDevice, activeCard, standbyCard string) { + // Wait for cards to become switchover ready. + SwitchoverReady(t, dut, activeCard, standbyCard) + switchoverResponse := gnoi.Execute(t, dut, system.NewSwitchControlProcessorOperation().Path(components.GetSubcomponentPath(standbyCard, deviations.GNOISubcomponentPath(dut)))) + t.Logf("gnoiClient.System().SwitchControlProcessor() response: %v", PrettyPrint(switchoverResponse)) + startSwitchover := time.Now() + t.Logf("Wait for new primary controller to boot up by polling the telemetry output ...") + for { + var currentTime string + t.Logf("Time elapsed %.2f seconds since switchover started.", time.Since(startSwitchover).Seconds()) + time.Sleep(30 * time.Second) + if errMsg := testt.CaptureFatal(t, func(t testing.TB) { + currentTime = gnmi.Get[string](t, dut, gnmi.OC().System().CurrentDatetime().State()) + }); errMsg != nil { + t.Logf("Got testt.CaptureFatal errMsg: %s, keep polling ...", *errMsg) + } else { + t.Logf("Controller switchover has completed successfully with received time: %v", currentTime) + break + } + if uint64(time.Since(startSwitchover).Seconds()) > maxSwitchoverTime { + t.Fatalf("time.Since(startSwitchover): got %v, want < %v", time.Since(startSwitchover), maxSwitchoverTime) + } + } + t.Logf("Controller switchover time: %.2f seconds.", time.Since(startSwitchover).Seconds()) +} diff --git a/internal/security/attestz/setup.go b/internal/security/attestz/setup.go new file mode 100644 index 00000000000..3028db99e25 --- /dev/null +++ b/internal/security/attestz/setup.go @@ -0,0 +1,204 @@ +// 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 attestz + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + "os" + "testing" + + cdpb "github.com/openconfig/attestz/proto/common_definitions" + attestzpb "github.com/openconfig/attestz/proto/tpm_attestz" + enrollzpb "github.com/openconfig/attestz/proto/tpm_enrollz" + "github.com/openconfig/featureprofiles/internal/components" + "github.com/openconfig/featureprofiles/internal/security/svid" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/binding/introspect" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +const ( + attestzServerName = "attestz-server" + sslProfileId = "tls-attestz" + mgmtVrf = "mgmtVrf" + attestzServerPort = 9000 + controlcardType = oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_CONTROLLER_CARD + cdpbActive = cdpb.ControlCardRole_CONTROL_CARD_ROLE_ACTIVE + cdpbStandby = cdpb.ControlCardRole_CONTROL_CARD_ROLE_STANDBY + primaryController = oc.Platform_ComponentRedundantRole_PRIMARY + secondaryController = oc.Platform_ComponentRedundantRole_SECONDARY +) + +// TlsConf struct holds mtls configs for attestz grpc connections. +type TlsConf struct { + Target string + Mtls bool + CaKeyFile string + CaCertFile string + ClientCert []byte + ClientKey []byte + ServerCert []byte + ServerKey []byte +} + +// NewAttestzSession creates a grpc client used in attestz tests. +func (tc *TlsConf) NewAttestzSession(t *testing.T) *AttestzSession { + tlsConf := new(tls.Config) + if tc.Mtls { + keyPair, err := tls.X509KeyPair(tc.ClientCert, tc.ClientKey) + if err != nil { + t.Fatalf("Error loading client keypair. error: %v", err) + } + tlsConf.Certificates = []tls.Certificate{keyPair} + caCertBytes, err := os.ReadFile(tc.CaCertFile) + if err != nil { + t.Fatalf("Error reading trust bundle file. error: %v", err) + } + trustBundle := x509.NewCertPool() + if !trustBundle.AppendCertsFromPEM(caCertBytes) { + t.Fatal("Error loading ca trust bundle.") + } + tlsConf.RootCAs = trustBundle + } else { + tlsConf.InsecureSkipVerify = true + } + + conn, err := grpc.NewClient( + tc.Target, + grpc.WithTransportCredentials(credentials.NewTLS(tlsConf)), + ) + + if err != nil { + t.Fatalf("Could not connect gnsi. error: %v", err) + } + return &AttestzSession{ + Conn: conn, + Peer: new(peer.Peer), + EnrollzClient: enrollzpb.NewTpmEnrollzServiceClient(conn), + AttestzClient: attestzpb.NewTpmAttestzServiceClient(conn), + } +} + +// EnableMtls creates client/server certificates signed by the given ca & pushes server certificate to the dut for a given ssl profile. +func (tc *TlsConf) EnableMtls(t *testing.T, dut *ondatra.DUTDevice, sslProfile string) { + caKey, caCert, err := svid.LoadKeyPair(tc.CaKeyFile, tc.CaCertFile) + if err != nil { + t.Fatalf("Error loading ca key/cert. error: %v", err) + } + clientIP, serverIP := getGrpcPeers(t, tc) + tc.ClientCert, tc.ClientKey, err = GenTlsCert(clientIP, caCert, caKey, caCert.PublicKeyAlgorithm) + if err != nil { + t.Fatalf("Error generating client tls certs. error: %s", err) + } + tc.ServerCert, tc.ServerKey, err = GenTlsCert(serverIP, caCert, caKey, caCert.PublicKeyAlgorithm) + if err != nil { + t.Fatalf("Error generating server tls certs. error: %s", err) + } + + caCertBytes, err := os.ReadFile(tc.CaCertFile) + if err != nil { + t.Fatalf("Error reading ca cert. error: %v", err) + } + RotateCerts(t, dut, CertTypeRaw, sslProfile, tc.ServerKey, tc.ServerCert, caCertBytes) + tc.Mtls = true +} + +// SetupBaseline setup a test ssl profile & grpc server to be used in attestz tests. +func SetupBaseline(t *testing.T, dut *ondatra.DUTDevice) (string, *oc.System_GrpcServer) { + AddProfile(t, dut, sslProfileId) + RotateCerts(t, dut, CertTypeIdevid, sslProfileId, nil, nil, nil) + gs := createTestGrpcServer(t, dut) + // Prepare target for the newly created gRPC Server + dialTarget := introspect.DUTDialer(t, dut, introspect.GNSI).DialTarget + resolvedTarget, err := net.ResolveTCPAddr("tcp", dialTarget) + if err != nil { + t.Fatalf("Failed resolving gnsi target %s", dialTarget) + } + resolvedTarget.Port = attestzServerPort + t.Logf("Target for new gNSI service: %s", resolvedTarget.String()) + return resolvedTarget.String(), gs +} + +func getGrpcPeers(t *testing.T, tc *TlsConf) (string, string) { + var as *AttestzSession + as = tc.NewAttestzSession(t) + defer as.Conn.Close() + // Make a test grpc call to get peer endpoints from the connection. + _, err := as.EnrollzClient.GetIakCert(context.Background(), &enrollzpb.GetIakCertRequest{ControlCardSelection: ParseRoleSelection(cdpbActive)}, grpc.Peer(as.Peer)) + if err != nil { + t.Fatalf("Error getting peer endpoints. error: %s", err) + } + localAddr := as.Peer.LocalAddr.(*net.TCPAddr) + remoteAddr := as.Peer.Addr.(*net.TCPAddr) + t.Logf("Got Local Address: %v, Remote Address: %v", localAddr, remoteAddr) + return localAddr.IP.String(), remoteAddr.IP.String() +} + +func createTestGrpcServer(t *testing.T, dut *ondatra.DUTDevice) *oc.System_GrpcServer { + t.Logf("Setting test grpc-server") + root := &oc.Root{} + s := root.GetOrCreateSystem() + gs := s.GetOrCreateGrpcServer(attestzServerName) + gs.SetEnable(true) + gs.SetPort(uint16(attestzServerPort)) + gs.SetCertificateId(sslProfileId) + gs.SetServices([]oc.E_SystemGrpc_GRPC_SERVICE{oc.SystemGrpc_GRPC_SERVICE_GNMI, oc.SystemGrpc_GRPC_SERVICE_GNSI}) + gs.SetMetadataAuthentication(false) + gs.SetNetworkInstance(mgmtVrf) + gnmi.Update(t, dut, gnmi.OC().System().Config(), s) + return gnmi.Get[*oc.System_GrpcServer](t, dut, gnmi.OC().System().GrpcServer(attestzServerName).State()) +} + +// Ensure that we can call both controllers. +func findControllers(t *testing.T, dut *ondatra.DUTDevice, controllers []string) (string, string) { + var primary, secondary string + for _, controller := range controllers { + role := gnmi.Get[oc.E_Platform_ComponentRedundantRole](t, dut, gnmi.OC().Component(controller).RedundantRole().State()) + t.Logf("Component(controller).RedundantRole().Get(t): %v, Role: %v", controller, role) + if role == secondaryController { + secondary = controller + } else if role == primaryController { + primary = controller + } else { + t.Fatalf("Expected controller %s to be active or standby, got %v", controller, role) + } + } + if secondary == "" || primary == "" { + t.Fatalf("Expected non-empty primary and secondary Controller, got primary: %v, secondary: %v", primary, secondary) + } + t.Logf("Detected primary: %v, secondary: %v", primary, secondary) + + return primary, secondary +} + +// FindControlCards finds active & standby control card on the dut. +func FindControlCards(t *testing.T, dut *ondatra.DUTDevice) (*ControlCard, *ControlCard) { + activeCard = &ControlCard{Role: cdpbActive} + standbyCard = &ControlCard{Role: cdpbStandby} + controllers := components.FindComponentsByType(t, dut, controlcardType) + t.Logf("Found controller list: %v", controllers) + if len(controllers) != 2 { + t.Skipf("Dual controllers required on %v: got %v, want 2", dut.Model(), len(controllers)) + } + activeCard.Name, standbyCard.Name = findControllers(t, dut, controllers) + return activeCard, standbyCard +}