| // Copyright 2023 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package networkui |
| |
| import ( |
| "context" |
| "fmt" |
| "time" |
| |
| "google.golang.org/protobuf/types/known/emptypb" |
| |
| "go.chromium.org/tast-tests/cros/common/crypto/certificate" |
| "go.chromium.org/tast-tests/cros/common/tbdep" |
| "go.chromium.org/tast-tests/cros/common/wifi/security/wpaeap" |
| cert "go.chromium.org/tast-tests/cros/remote/network" |
| "go.chromium.org/tast-tests/cros/remote/wificell" |
| "go.chromium.org/tast-tests/cros/remote/wificell/hostapd" |
| "go.chromium.org/tast-tests/cros/services/cros/networkui" |
| "go.chromium.org/tast-tests/cros/services/cros/ui" |
| "go.chromium.org/tast-tests/cros/services/cros/wifi" |
| "go.chromium.org/tast/core/ctxutil" |
| "go.chromium.org/tast/core/errors" |
| "go.chromium.org/tast/core/rpc" |
| "go.chromium.org/tast/core/ssh" |
| "go.chromium.org/tast/core/testing" |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: CertsUsableAndPersist, |
| LifeCycleStage: testing.LifeCycleOwnerMonitored, |
| Desc: "Verify that installed certificates are usable and persist after different test scenarios", |
| Contacts: []string{ |
| "cros-connectivity@google.com", |
| "chromeos-connectivity-engprod@google.com", |
| "shijinabraham@google.com", |
| "chadduffin@chromium.org", |
| }, |
| BugComponent: "b:1318544", // ChromeOS > Software > System Services > Connectivity > General |
| Attr: []string{"group:wificell", "wificell_e2e"}, |
| TestBedDeps: []string{tbdep.Wificell, tbdep.WifiStateNormal, tbdep.BluetoothStateNormal, tbdep.PeripheralWifiStateWorking}, |
| ServiceDeps: []string{ |
| "tast.cros.browser.ChromeService", |
| "tast.cros.ui.ChromeUIService", |
| "tast.cros.networkui.CertificateService", |
| "tast.cros.wifi.WifiService", |
| wificell.ShillServiceName, |
| }, |
| SoftwareDeps: []string{"chrome"}, |
| VarDeps: certsUsableAndPersistVars(), |
| Fixture: wificell.FixtureID(wificell.TFFeaturesNone), |
| Params: []testing.Param{ |
| { |
| Name: "suspend", |
| Val: &certsUsableTestParams{ |
| testScenario: suspendAndRestore, |
| numTrials: 1, |
| // WiFi should be auto-reconnected on resume, wait for it to reconnect. |
| shouldAutoConnect: true, |
| }, |
| }, { |
| Name: "relogin", |
| Val: &certsUsableTestParams{ |
| testScenario: reLoginAndRestore, |
| numTrials: 1, |
| }, |
| }, { |
| Name: "relogin_stress", |
| Val: &certsUsableTestParams{ |
| testScenario: reLoginAndRestore, |
| numTrials: 3, |
| }, |
| }, |
| }, |
| Timeout: 15 * time.Minute, |
| }) |
| } |
| |
| type certsUsableTestParams struct { |
| // testScenario is the action to be tested, certificate should be usable and persist after this action. |
| testScenario func(ctx context.Context, tf *wificell.TestFixture) error |
| |
| // numTrials is a number of the trials for the test scenario. |
| numTrials int |
| |
| shouldAutoConnect bool |
| } |
| |
| type certificateDetail struct { |
| *networkui.Certificate |
| certificate.CertStore |
| } |
| |
| type certificates map[networkui.Certificate_Type]*certificateDetail |
| |
| // CertsUsableAndPersist verifies that installed certificates are usable and persist after different test scenarios. |
| func CertsUsableAndPersist(ctx context.Context, s *testing.State) { |
| testCerts := cert.LoadCertsFromVars() |
| |
| tf := s.FixtValue().(*wificell.TestFixture) |
| |
| opt := []hostapd.Option{hostapd.Mode(hostapd.Mode80211g), hostapd.Channel(1)} |
| cfg := wpaeap.NewConfigFactory( |
| testCerts.CACred.Cert, |
| testCerts.ServerCred, |
| wpaeap.ClientCACert(testCerts.CACred.Cert), |
| wpaeap.ClientCred(testCerts.ClientCred), |
| ) |
| |
| ap, err := tf.ConfigureAP(ctx, opt, cfg) |
| if err != nil { |
| s.Fatal("Failed to configure ap: ", err) |
| } |
| defer tf.DeconfigAP(ctx, ap) |
| ctx, cancel := tf.ReserveForDeconfigAP(ctx, ap) |
| defer cancel() |
| |
| cleanupCtx := ctx |
| ctx, cancel = ctxutil.Shorten(ctx, 10*time.Second) |
| defer cancel() |
| |
| params := s.Param().(*certsUsableTestParams) |
| |
| startChromeReq := &ui.NewRequest{} |
| |
| rpcClient := tf.DUTRPC(wificell.DefaultDUT) |
| crSvc := ui.NewChromeServiceClient(rpcClient.Conn) |
| if _, err := crSvc.New(ctx, startChromeReq); err != nil { |
| s.Fatal("Failed to start Chrome: ", err) |
| } |
| defer crSvc.Close(cleanupCtx, &emptypb.Empty{}) |
| |
| certSvc := networkui.NewCertificateServiceClient(rpcClient.Conn) |
| if _, err := certSvc.Init(ctx, &networkui.InitRequest{ |
| InitType: networkui.InitRequest_LAUNCH, |
| }); err != nil { |
| s.Fatal("Failed to initialize the certificate service: ", err) |
| } |
| defer certSvc.Close(cleanupCtx, &emptypb.Empty{}) |
| |
| for _, test := range []struct { |
| name string |
| certDetails map[networkui.Certificate_Type]*certificateDetail |
| }{ |
| { |
| name: "password-protected certificate", |
| certDetails: map[networkui.Certificate_Type]*certificateDetail{ |
| networkui.Certificate_CLIENT: { |
| Certificate: &networkui.Certificate{ |
| Type: networkui.Certificate_CLIENT, |
| Name: testCerts.ClientCred.Info.CommonName, |
| Organization: testCerts.ClientCred.Info.Organization, |
| Password: "12345", |
| }, |
| CertStore: testCerts, |
| }, |
| networkui.Certificate_CA: { |
| Certificate: &networkui.Certificate{ |
| Type: networkui.Certificate_CA, |
| Name: testCerts.CACred.Info.CommonName, |
| Organization: testCerts.CACred.Info.Organization, |
| }, |
| CertStore: testCerts, |
| }, |
| }, |
| }, { |
| name: "non-password-protected certificate", |
| certDetails: map[networkui.Certificate_Type]*certificateDetail{ |
| networkui.Certificate_CLIENT: { |
| Certificate: &networkui.Certificate{ |
| Type: networkui.Certificate_CLIENT, |
| Name: testCerts.ClientCred.Info.CommonName, |
| Organization: testCerts.ClientCred.Info.Organization, |
| }, |
| CertStore: testCerts, |
| }, |
| networkui.Certificate_CA: { |
| Certificate: &networkui.Certificate{ |
| Type: networkui.Certificate_CA, |
| Name: testCerts.CACred.Info.CommonName, |
| Organization: testCerts.CACred.Info.Organization, |
| }, |
| CertStore: testCerts, |
| }, |
| }, |
| }, |
| } { |
| s.Run(ctx, test.name, func(ctx context.Context, s *testing.State) { |
| // certSvc.DeleteCert is a combination of UI actions, which could take a while. |
| const deleteCertTimeout = 35 * time.Second |
| |
| cleanupCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, deleteCertTimeout) |
| defer cancel() |
| |
| for _, certDetail := range test.certDetails { |
| if err := importCert(ctx, tf.DUTConn(wificell.DefaultDUT), certSvc, certDetail); err != nil { |
| s.Fatal("Failed to import the certificate: ", err) |
| } |
| defer certSvc.DeleteCert(cleanupCtx, certDetail.Certificate) |
| } |
| defer dumpUITreeWithScreenshotOnError(cleanupCtx, rpcClient, s.HasError, "before_delete_certs") |
| |
| if err := certsImported(ctx, certSvc, test.certDetails); err != nil { |
| s.Fatal("Failed to verify certificates are all imported: ", err) |
| } |
| |
| joinEAPNetworkReq := &wifi.JoinWifiRequest{ |
| Ssid: ap.Config().SSID, |
| Security: &wifi.JoinWifiRequest_EapTls{ |
| EapTls: &wifi.JoinWifiRequest_SecurityEapTls{ |
| ClientCert: fmt.Sprintf("%s [%s]", |
| test.certDetails[networkui.Certificate_CA].Name, |
| test.certDetails[networkui.Certificate_CLIENT].Name), |
| CaCert: fmt.Sprintf("%s [%s]", |
| test.certDetails[networkui.Certificate_CA].Name, |
| test.certDetails[networkui.Certificate_CA].Name), |
| }, |
| }, |
| } |
| defer tf.CleanDisconnectDUTFromWifi(cleanupCtx, wificell.DefaultDUT) |
| |
| if err := verifyCertUsable(ctx, tf, joinEAPNetworkReq, ap.Config().SSID); err != nil { |
| s.Fatal("Failed to verify that the certificates are usable: ", err) |
| } |
| |
| for i := 0; i < params.numTrials; i++ { |
| if err := params.testScenario(ctx, tf); err != nil { |
| s.Fatal("Failed to complete the test scenario: ", err) |
| } |
| } |
| |
| if params.shouldAutoConnect { |
| wifiClient := tf.DUTWifiClient(wificell.DefaultDUT) |
| if err := wifiClient.WaitForConnected(ctx, ap.Config().SSID, true); err != nil { |
| s.Fatalf("Failed to wait for WiFi %q to connect: %v", ap.Config().SSID, err) |
| } |
| |
| // Disconnect from WiFi and cleanup configurations to further verify that certificates are usable and persist. |
| if err := tf.CleanDisconnectDUTFromWifi(cleanupCtx, wificell.DefaultDUT); err != nil { |
| s.Fatal("Failed to disconnect from WiFi and cleanup WiFi configurations: ", err) |
| } |
| } |
| |
| // Verify the certificates are still persist. |
| if err := certsImported(ctx, certSvc, test.certDetails); err != nil { |
| s.Fatal("Failed to verify certificates are all imported: ", err) |
| } |
| |
| // Verify the certificates are still usable. |
| if err := verifyCertUsable(ctx, tf, joinEAPNetworkReq, ap.Config().SSID); err != nil { |
| s.Fatal("Failed to verify that the certificates are usable and persist: ", err) |
| } |
| }) |
| } |
| } |
| |
| // suspendAndRestore suspends the DUT for a while and then |
| // restores resources associate with chrome.Chrome instance, |
| // including chrome.Chrome instance and certificate manager. |
| func suspendAndRestore(ctx context.Context, tf *wificell.TestFixture) error { |
| wifiClient := tf.DUTWifiClient(wificell.DefaultDUT) |
| if err := wifiClient.Suspend(ctx, 5*time.Second); err != nil { |
| return errors.Wrap(err, "failed to suspend DUT") |
| } |
| |
| rpcClient := tf.DUTRPC(wificell.DefaultDUT) |
| crSvc := ui.NewChromeServiceClient(rpcClient.Conn) |
| if _, err := crSvc.Reconnect(ctx, &emptypb.Empty{}); err != nil { |
| return errors.Wrap(err, "failed to reconnect to the Chrome session") |
| } |
| |
| certSvc := networkui.NewCertificateServiceClient(rpcClient.Conn) |
| if _, err := certSvc.Init(ctx, &networkui.InitRequest{ |
| InitType: networkui.InitRequest_CONNECT, |
| }); err != nil { |
| return errors.Wrap(err, "failed to reconnect to the certificate service") |
| } |
| |
| return nil |
| } |
| |
| // reLoginAndRestore re-login the chrome.Chrome session by starting a new one. |
| // This method also ensure resources are all well-managed by closing them before re-login. |
| func reLoginAndRestore(ctx context.Context, tf *wificell.TestFixture) error { |
| rpcClient := tf.DUTRPC(wificell.DefaultDUT) |
| |
| certSvc := networkui.NewCertificateServiceClient(rpcClient.Conn) |
| if _, err := certSvc.Close(ctx, &emptypb.Empty{}); err != nil { |
| return errors.Wrap(err, "failed to close the certificate manager") |
| } |
| |
| crSvc := ui.NewChromeServiceClient(rpcClient.Conn) |
| if _, err := crSvc.Close(ctx, &emptypb.Empty{}); err != nil { |
| return errors.Wrap(err, "failed to close the chrome session") |
| } |
| |
| startChromeReq := &ui.NewRequest{KeepState: true} |
| |
| if _, err := crSvc.New(ctx, startChromeReq); err != nil { |
| return errors.Wrap(err, "failed to re-login to the chrome") |
| } |
| |
| if _, err := certSvc.Init(ctx, &networkui.InitRequest{ |
| InitType: networkui.InitRequest_LAUNCH, |
| }); err != nil { |
| return errors.Wrap(err, "failed to initialize the certificate service") |
| } |
| |
| return nil |
| } |
| |
| // verifyCertUsable verifies that the certificates are available and usable by trying to join to the network. |
| // Joining a WEP network requires a certificate, so the join attempt ensures that certificates are available. |
| // This method also expects the network to be connected, which means that the certificates are usable. |
| func verifyCertUsable(ctx context.Context, tf *wificell.TestFixture, joinEAPNetworkReq *wifi.JoinWifiRequest, ssid string) error { |
| wifiUIClient := wifi.NewWifiServiceClient(tf.DUTRPC(wificell.DefaultDUT).Conn) |
| if _, err := wifiUIClient.JoinWifiFromQuickSettings(ctx, joinEAPNetworkReq); err != nil { |
| return errors.Wrapf(err, "failed to connect to WiFi %q", ssid) |
| } |
| |
| wifiClient := tf.DUTWifiClient(wificell.DefaultDUT) |
| if err := wifiClient.WaitForConnected(ctx, ssid, true); err != nil { |
| return errors.Wrapf(err, "failed to wait for WiFi %q to connect", ssid) |
| } |
| return nil |
| } |
| |
| // certsImported verifies that |certificates| are all imported. |
| func certsImported(ctx context.Context, certSvc networkui.CertificateServiceClient, certificates certificates) error { |
| for _, cert := range certificates { |
| if response, err := certSvc.IsCertImported(ctx, cert.Certificate); err != nil { |
| return errors.Wrapf(err, "failed to verify certificate %+v is imported", cert.Certificate) |
| } else if !response.IsImported { |
| return errors.Errorf("certificate %+v is not imported", cert.Certificate) |
| } |
| } |
| return nil |
| } |
| |
| func importCert(ctx context.Context, dutConn *ssh.Conn, certSvc networkui.CertificateServiceClient, certDetail *certificateDetail) (retErr error) { |
| cleanupCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, 3*time.Second) |
| defer cancel() |
| |
| req := &networkui.ImportRequest{Certificate: certDetail.Certificate} |
| |
| switch certDetail.Type { |
| case networkui.Certificate_CLIENT: |
| dest := certificate.NewRemoteDestination(dutConn, "/tmp", "test_cert_client.p12") |
| cleanUp, err := certificate.WriteClientCertWithPassword(ctx, dest, certDetail.CertStore, certDetail.Password) |
| if err != nil { |
| return errors.Wrap(err, "failed to create the client certificate file") |
| } |
| defer cleanUp(cleanupCtx) |
| req.FilePath = dest.FullPath() |
| |
| req.ImportDetail = &networkui.ImportRequest_Client{ |
| Client: &networkui.ImportRequest_ClientImportDetail{ |
| ImportType: networkui.ImportRequest_ClientImportDetail_IMPORT_AND_BIND, |
| }, |
| } |
| case networkui.Certificate_CA: |
| dest := certificate.NewRemoteDestination(dutConn, "/tmp", "test_cert_root.crt") |
| cleanUp, err := certificate.WriteCACert(ctx, dest, certDetail.CertStore) |
| if err != nil { |
| return errors.Wrap(err, "failed to create the CA certificate file") |
| } |
| defer cleanUp(cleanupCtx) |
| req.FilePath = dest.FullPath() |
| |
| req.ImportDetail = &networkui.ImportRequest_Ca{ |
| Ca: &networkui.ImportRequest_CaImportDetail{}, |
| } |
| } |
| |
| if _, err := certSvc.ImportCert(ctx, req); err != nil { |
| return errors.Wrapf(err, "failed to import the certificate %+v", certDetail.Certificate) |
| } |
| |
| return nil |
| } |
| |
| func dumpUITreeWithScreenshotOnError(ctx context.Context, rpcClient *rpc.Client, hasError func() bool, filePrefix string) { |
| if hasError() { |
| ui.NewChromeUIServiceClient(rpcClient.Conn).DumpUITreeWithScreenshotToFile(ctx, &ui.DumpUITreeWithScreenshotToFileRequest{FilePrefix: filePrefix}) |
| } |
| } |
| |
| func certsUsableAndPersistVars() []string { |
| variables := make([]string, 0, 12) |
| |
| variables = append(variables, cert.CaCertificateVariables...) |
| variables = append(variables, cert.ServerCertificateVariables...) |
| variables = append(variables, cert.ClientCertificateVariables...) |
| |
| return variables |
| } |