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 +}