Move signing functionality to module.

To optimize the code signing steps we are moving the funtionality to a
module. This new approach allows us to code sign as soon as the
artifacts are generated and within a bcid context to avoid provenance
overriding.

Bug: https://github.com/flutter/flutter/issues/124055
Change-Id: I8eb7f3901bf0915581843c03b18fe59294ca309a
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/41522
Reviewed-by: Xilai Zhang <xilaizhang@google.com>
Reviewed-by: Ricardo Amador <ricardoamador@google.com>
Commit-Queue: Godofredo Contreras <godofredoc@google.com>
(cherry picked from commit f67ac6d30498f4603c0355e804e58e39cbd194d2)
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/41663
diff --git a/recipe_modules/signing/__init__.py b/recipe_modules/signing/__init__.py
new file mode 100644
index 0000000..0c7164d
--- /dev/null
+++ b/recipe_modules/signing/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2023 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+DEPS = [
+    'flutter/flutter_deps',
+    'flutter/kms',
+    'flutter/zip',
+    'recipe_engine/context',
+    'recipe_engine/file',
+    'recipe_engine/futures',
+    'recipe_engine/path',
+    'recipe_engine/platform',
+    'recipe_engine/raw_io',
+    'recipe_engine/runtime',
+    'recipe_engine/step',
+]
diff --git a/recipe_modules/signing/api.py b/recipe_modules/signing/api.py
new file mode 100644
index 0000000..71cfc09
--- /dev/null
+++ b/recipe_modules/signing/api.py
@@ -0,0 +1,252 @@
+# Copyright 2023 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+#import zipfile
+from recipe_engine import recipe_api
+
+
+# File name inside artifacts that require signing with entitlements.
+ENTITLEMENTS_FILENAME = 'entitlements.txt'
+# File name inside artifacts that require signing without entitlements.
+WITHOUT_ENTITLEMENTS_FILENAME = 'without_entitlements.txt'
+
+
+class CodeSignApi(recipe_api.RecipeApi):
+  """Provides utilities to code sign binaries in mac."""
+
+  def __init__(self, *args, **kwargs):
+    super().__init__(*args, **kwargs)
+    self._initialized = False
+    self._codesign_binary_path = None
+
+
+  def requires_signing(self, artifact_path):
+    """Validates if a file needs to be codesigned.
+
+    Args:
+      artifact_path: (str) the path to the zip file artifact
+        to validate if code signing is required.
+
+    Returns:
+      True if code sign is required, False if it is not.
+    """
+    if not self.m.platform.is_mac:
+      return False
+    file_list = self.m.zip.namelist('namelist', artifact_path)
+    return (
+        ENTITLEMENTS_FILENAME in file_list or
+        WITHOUT_ENTITLEMENTS_FILENAME in file_list)
+
+  @property
+  def codesign_binary(self):
+    self._ensure()
+    return self._codesign_binary_path
+
+  def _start(self, env, env_prefixes):
+    self._ensure()
+    self._codesign_environment(env, env_prefixes)
+    self._keychain_setup(env, env_prefixes)
+
+
+  def _stop(self):
+    self._keychain_cleanup()
+
+
+  def _ensure(self):
+    if not self._codesign_binary_path:
+      with self.m.step.nest('Codesign Dependencies'):
+        self._codesign_binary_path = self.m.flutter_deps.codesign({},{})
+
+  def code_sign(self, files_to_sign):
+    if not self.m.platform.is_mac:
+      return
+    env = {}
+    env_prefixes = {}
+    self._start(env, env_prefixes)
+    try:
+      self._signer_tasks(env, env_prefixes, files_to_sign)
+    finally:
+      if not self.m.runtime.in_global_shutdown:
+        self._stop()
+
+
+  def _codesign_environment(self, env, env_prefixes):
+    with self.m.step.nest('Setup codesign environment'):
+      secrets_dict = {
+          'FLUTTER_P12': 'flutter_p12.encrypted',
+          'FLUTTER_P12_PASSWORD': 'p12_password.encrypted',
+          'CODESIGN_TEAM_ID': 'codesign_team_id.encrypted',
+          'CODESIGN_APP_SPECIFIC_PASSWORD':
+              'codesign_app_specific_password.encrypted',
+          'CODESIGN_APP_STORE_ID': 'codesign_app_store_id.encrypted'
+      }
+      self.m.kms.decrypt_secrets(env, secrets_dict)
+
+
+  def _keychain_setup(self, env, env_prefixes):
+    """KeychainSetup adds flutter .p12 to a temporary keychain named 'build'.
+
+    Args:
+      codesign_path (str): path of codesign cipd package.
+      p12_filepath (str) : path of the .p12 file that has flutter credentials.
+      p12_password_raw (str) : the password to decode the .p12 flutter file.
+    """
+    with self.m.step.nest('Setup keychain'):
+      # Delete build.keychain if exists.
+      self.m.step(
+          'delete previous keychain',
+          ['security', 'delete-keychain', 'build.keychain'],
+          ok_ret='any'
+      )
+      # Create build.keychain.
+      self.m.step(
+          'create keychain',
+          ['security', 'create-keychain', '-p', '', 'build.keychain']
+      )
+      # Set build.keychain as default.
+      self.m.step(
+          'default keychain',
+          ['security', 'default-keychain', '-s', 'build.keychain']
+      )
+      # Unlock build.keychain to allow sign commands to use its secrets.
+      self.m.step(
+          'unlock build keychain',
+          ['security', 'unlock-keychain', '-p', '', 'build.keychain']
+      )
+      # Import flutter's certificate to the keychain.
+      self._import_certificate(env, env_prefixes)
+      # Sets a partition list to identify the app signatures allowed to use the key.
+      self.m.step(
+          'set key partition list', [
+              'security', 'set-key-partition-list', '-S',
+              'apple-tool:,apple:,codesign:', '-s', '-k', '', 'build.keychain'
+          ]
+      )
+      # Grabs existing identities to find out if we are ready to sign with flutter's
+      # identity.
+      show_identities_step = self.m.step(
+          'show-identities', ['security', 'find-identity', '-v'],
+          ok_ret='any',
+          stdout=self.m.raw_io.output_text(),
+          stderr=self.m.raw_io.output_text()
+      )
+      flutter_identity_name = 'FLUTTER.IO LLC'
+      if flutter_identity_name not in show_identities_step.stdout:
+        raise ValueError(
+           'identities are %s, does not include flutter identity' %
+           (show_identities_step.stdout)
+        )
+
+  def _import_certificate(self, env, env_prefixes):
+    """Import flutter codesign identity into keychain.
+
+    This function triggers a shell script that supplies p12 password,
+    and grants codesign cipd and system codesign the correct access controls.
+    The p12 password is hidden from stdout.
+
+    Args:
+      env (dict): environment variables.
+      env_prefixes (dict) : environment paths.
+    """
+    resource_name = self.resource('import_certificate.sh')
+    self.m.step(
+        'Set execute permission',
+        ['chmod', '755', resource_name],
+        infra_step=True,
+    )
+    # Only filepath with a .p12 suffix will be recognized.
+    p12_suffix_filepath = self.m.path['cleanup'].join('flutter.p12')
+    env['P12_SUFFIX_FILEPATH'] = p12_suffix_filepath
+    with self.m.context(env=env, env_prefixes=env_prefixes):
+      self.m.step('import certificate', [resource_name])
+
+
+  def _signer_tasks(self, env, env_prefixes, files_to_sign):
+    """Concurrently creates jobs to codesign each binary.
+
+    Args:
+      codesign_path (str): path of codesign cipd package.
+      env (dict): environment variables.
+      env_prefixes (dict) : environment paths.
+    """
+    signer_builds = []
+    for source_path in files_to_sign:
+      signer_builds.append(
+        self.m.futures.spawn(
+            self._run_signer_tool_command,
+            env,
+            env_prefixes,
+            source_path,
+        )
+      )
+
+    futures = self.m.futures.wait(signer_builds)
+    for future in futures:
+      future.result()
+
+  def _run_signer_tool_command(
+      self,
+      env,
+      env_prefixes,
+      source_path,
+  ):
+    """Runs code sign standalone app.
+
+    Args:
+      source_path (Path): path of the artifact to sign.
+      app_specific_password_filepath (str) : path of app specific password, one of
+      the code sign credentials.
+      appstore_id_filepath (str) : path of apple store id, one of the codesign
+      credentials.
+      team_id_filepath (str) : path of flutter team id used for codesign, one of the
+      codesign credentials.
+      codesign_string_path (str): the absolute path of the codesign standalone app
+      cipd package. This is to differentiate codesign cipd from mac system codesign.
+    """
+    app_specific_password_filepath = env['CODESIGN_APP_SPECIFIC_PASSWORD']
+    appstore_id_filepath = env['CODESIGN_APP_STORE_ID']
+    team_id_filepath = env['CODESIGN_TEAM_ID']
+    path, base_name = self.m.path.split(source_path)
+    unsigned_path = self.m.path.join(path, base_name)
+    self.m.file.move(
+        'Move %s' % str(source_path),
+        source_path,
+        unsigned_path
+    )
+    with self.m.step.nest('Codesign %s' % str(unsigned_path)):
+      flutter_certificate_name = 'FLUTTER.IO LLC'
+      self.m.step(
+          'unlock build keychain',
+          ['security', 'unlock-keychain', '-p', '', 'build.keychain']
+      )
+      with self.m.context(env=env, env_prefixes=env_prefixes):
+        self.m.step(
+            'codesign Apple engine binaries',
+            [
+                self.codesign_binary,
+                '--codesign-cert-name',
+                flutter_certificate_name,
+                '--no-dryrun',
+                '--app-specific-password-file-path',
+                app_specific_password_filepath,
+                '--codesign-appstore-id-file-path',
+                appstore_id_filepath,
+                '--codesign-team-id-file-path',
+                team_id_filepath,
+                '--input-zip-file-path',
+                str(unsigned_path),
+                '--output-zip-file-path',
+                str(source_path),
+            ],
+        )
+
+  def _keychain_cleanup(self):
+    """Clean up temporary keychain used in codesign process."""
+    with self.m.step.nest('Keychain cleanup'):
+      self.m.step('delete keychain', ['security', 'delete-keychain', 'build.keychain'])
+      self.m.step(
+         'Cleanup keychain.restore default keychain',
+         ['security', 'default-keychain', '-s', 'login.keychain']
+      )
+
diff --git a/recipe_modules/signing/examples/code_sign.expected/mac_require_signing.json b/recipe_modules/signing/examples/code_sign.expected/mac_require_signing.json
new file mode 100644
index 0000000..e07bd77
--- /dev/null
+++ b/recipe_modules/signing/examples/code_sign.expected/mac_require_signing.json
@@ -0,0 +1,556 @@
+[
+  {
+    "cmd": [],
+    "name": "Codesign Dependencies"
+  },
+  {
+    "cmd": [],
+    "name": "Codesign Dependencies.Installing Mac codesign CIPD pkg",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1",
+      "-ensure-file",
+      "flutter/codesign/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Codesign Dependencies.Installing Mac codesign CIPD pkg.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"flutter/codesign/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "Setup codesign environment"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/flutter_p12.encrypted",
+      "[CLEANUP]/flutter_p12.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/flutter_p12.encrypted",
+      "-output",
+      "[CLEANUP]/FLUTTER_P12",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/p12_password.encrypted",
+      "[CLEANUP]/p12_password.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/p12_password.encrypted",
+      "-output",
+      "[CLEANUP]/FLUTTER_P12_PASSWORD",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (3)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/codesign_team_id.encrypted",
+      "[CLEANUP]/codesign_team_id.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (3)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/codesign_team_id.encrypted",
+      "-output",
+      "[CLEANUP]/CODESIGN_TEAM_ID",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (3)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (4)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/codesign_app_specific_password.encrypted",
+      "[CLEANUP]/codesign_app_specific_password.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (4)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/codesign_app_specific_password.encrypted",
+      "-output",
+      "[CLEANUP]/CODESIGN_APP_SPECIFIC_PASSWORD",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (4)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (5)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/codesign_app_store_id.encrypted",
+      "[CLEANUP]/codesign_app_store_id.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (5)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/codesign_app_store_id.encrypted",
+      "-output",
+      "[CLEANUP]/CODESIGN_APP_STORE_ID",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (5)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "Setup keychain"
+  },
+  {
+    "cmd": [
+      "security",
+      "delete-keychain",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.delete previous keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "create-keychain",
+      "-p",
+      "",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.create keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "default-keychain",
+      "-s",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.default keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "unlock-keychain",
+      "-p",
+      "",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.unlock build keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "chmod",
+      "755",
+      "RECIPE_MODULE[flutter::signing]/resources/import_certificate.sh"
+    ],
+    "infra_step": true,
+    "name": "Setup keychain.Set execute permission",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "RECIPE_MODULE[flutter::signing]/resources/import_certificate.sh"
+    ],
+    "env": {
+      "CODESIGN_APP_SPECIFIC_PASSWORD": "[CLEANUP]/CODESIGN_APP_SPECIFIC_PASSWORD",
+      "CODESIGN_APP_STORE_ID": "[CLEANUP]/CODESIGN_APP_STORE_ID",
+      "CODESIGN_TEAM_ID": "[CLEANUP]/CODESIGN_TEAM_ID",
+      "FLUTTER_P12": "[CLEANUP]/FLUTTER_P12",
+      "FLUTTER_P12_PASSWORD": "[CLEANUP]/FLUTTER_P12_PASSWORD",
+      "P12_SUFFIX_FILEPATH": "[CLEANUP]/flutter.p12"
+    },
+    "name": "Setup keychain.import certificate",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "set-key-partition-list",
+      "-S",
+      "apple-tool:,apple:,codesign:",
+      "-s",
+      "-k",
+      "",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.set key partition list",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "find-identity",
+      "-v"
+    ],
+    "name": "Setup keychain.show-identities",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "move",
+      "file1.zip",
+      "file1.zip"
+    ],
+    "infra_step": true,
+    "name": "Move file1.zip"
+  },
+  {
+    "cmd": [],
+    "name": "Codesign file1.zip"
+  },
+  {
+    "cmd": [
+      "security",
+      "unlock-keychain",
+      "-p",
+      "",
+      "build.keychain"
+    ],
+    "name": "Codesign file1.zip.unlock build keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/codesign",
+      "--codesign-cert-name",
+      "FLUTTER.IO LLC",
+      "--no-dryrun",
+      "--app-specific-password-file-path",
+      "[CLEANUP]/CODESIGN_APP_SPECIFIC_PASSWORD",
+      "--codesign-appstore-id-file-path",
+      "[CLEANUP]/CODESIGN_APP_STORE_ID",
+      "--codesign-team-id-file-path",
+      "[CLEANUP]/CODESIGN_TEAM_ID",
+      "--input-zip-file-path",
+      "file1.zip",
+      "--output-zip-file-path",
+      "file1.zip"
+    ],
+    "env": {
+      "CODESIGN_APP_SPECIFIC_PASSWORD": "[CLEANUP]/CODESIGN_APP_SPECIFIC_PASSWORD",
+      "CODESIGN_APP_STORE_ID": "[CLEANUP]/CODESIGN_APP_STORE_ID",
+      "CODESIGN_TEAM_ID": "[CLEANUP]/CODESIGN_TEAM_ID",
+      "FLUTTER_P12": "[CLEANUP]/FLUTTER_P12",
+      "FLUTTER_P12_PASSWORD": "[CLEANUP]/FLUTTER_P12_PASSWORD",
+      "P12_SUFFIX_FILEPATH": "[CLEANUP]/flutter.p12"
+    },
+    "name": "Codesign file1.zip.codesign Apple engine binaries",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "Keychain cleanup"
+  },
+  {
+    "cmd": [
+      "security",
+      "delete-keychain",
+      "build.keychain"
+    ],
+    "name": "Keychain cleanup.delete keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "default-keychain",
+      "-s",
+      "login.keychain"
+    ],
+    "name": "Keychain cleanup.Cleanup keychain.restore default keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/code_sign.expected/no_signing_identity.json b/recipe_modules/signing/examples/code_sign.expected/no_signing_identity.json
new file mode 100644
index 0000000..186f496
--- /dev/null
+++ b/recipe_modules/signing/examples/code_sign.expected/no_signing_identity.json
@@ -0,0 +1,471 @@
+[
+  {
+    "cmd": [],
+    "name": "Codesign Dependencies"
+  },
+  {
+    "cmd": [],
+    "name": "Codesign Dependencies.Installing Mac codesign CIPD pkg",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1",
+      "-ensure-file",
+      "flutter/codesign/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Codesign Dependencies.Installing Mac codesign CIPD pkg.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"flutter/codesign/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "Setup codesign environment"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/flutter_p12.encrypted",
+      "[CLEANUP]/flutter_p12.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/flutter_p12.encrypted",
+      "-output",
+      "[CLEANUP]/FLUTTER_P12",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/p12_password.encrypted",
+      "[CLEANUP]/p12_password.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/p12_password.encrypted",
+      "-output",
+      "[CLEANUP]/FLUTTER_P12_PASSWORD",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (3)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/codesign_team_id.encrypted",
+      "[CLEANUP]/codesign_team_id.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (3)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/codesign_team_id.encrypted",
+      "-output",
+      "[CLEANUP]/CODESIGN_TEAM_ID",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (3)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (4)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/codesign_app_specific_password.encrypted",
+      "[CLEANUP]/codesign_app_specific_password.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (4)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/codesign_app_specific_password.encrypted",
+      "-output",
+      "[CLEANUP]/CODESIGN_APP_SPECIFIC_PASSWORD",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (4)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cloudkms",
+      "-ensure-file",
+      "infra/tools/luci/cloudkms/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "Setup codesign environment.ensure_installed (5)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/cloudkms/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "gs://flutter_configs/codesign_app_store_id.encrypted",
+      "[CLEANUP]/codesign_app_store_id.encrypted"
+    ],
+    "infra_step": true,
+    "name": "Setup codesign environment.gsutil download (5)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cloudkms/cloudkms",
+      "decrypt",
+      "-input",
+      "[CLEANUP]/codesign_app_store_id.encrypted",
+      "-output",
+      "[CLEANUP]/CODESIGN_APP_STORE_ID",
+      "projects/flutter-infra-staging/locations/global/keyRings/luci/cryptoKeys/flutter-infra"
+    ],
+    "name": "Setup codesign environment.cloudkms get key (5)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "Setup keychain",
+    "~followup_annotations": [
+      "@@@STEP_EXCEPTION@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "delete-keychain",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.delete previous keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "create-keychain",
+      "-p",
+      "",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.create keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "default-keychain",
+      "-s",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.default keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "unlock-keychain",
+      "-p",
+      "",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.unlock build keychain",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "chmod",
+      "755",
+      "RECIPE_MODULE[flutter::signing]/resources/import_certificate.sh"
+    ],
+    "infra_step": true,
+    "name": "Setup keychain.Set execute permission",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "RECIPE_MODULE[flutter::signing]/resources/import_certificate.sh"
+    ],
+    "env": {
+      "CODESIGN_APP_SPECIFIC_PASSWORD": "[CLEANUP]/CODESIGN_APP_SPECIFIC_PASSWORD",
+      "CODESIGN_APP_STORE_ID": "[CLEANUP]/CODESIGN_APP_STORE_ID",
+      "CODESIGN_TEAM_ID": "[CLEANUP]/CODESIGN_TEAM_ID",
+      "FLUTTER_P12": "[CLEANUP]/FLUTTER_P12",
+      "FLUTTER_P12_PASSWORD": "[CLEANUP]/FLUTTER_P12_PASSWORD",
+      "P12_SUFFIX_FILEPATH": "[CLEANUP]/flutter.p12"
+    },
+    "name": "Setup keychain.import certificate",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "set-key-partition-list",
+      "-S",
+      "apple-tool:,apple:,codesign:",
+      "-s",
+      "-k",
+      "",
+      "build.keychain"
+    ],
+    "name": "Setup keychain.set key partition list",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "security",
+      "find-identity",
+      "-v"
+    ],
+    "name": "Setup keychain.show-identities",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/code_sign.expected/non_mac.json b/recipe_modules/signing/examples/code_sign.expected/non_mac.json
new file mode 100644
index 0000000..b6042b6
--- /dev/null
+++ b/recipe_modules/signing/examples/code_sign.expected/non_mac.json
@@ -0,0 +1,5 @@
+[
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/code_sign.py b/recipe_modules/signing/examples/code_sign.py
new file mode 100644
index 0000000..8b4370c
--- /dev/null
+++ b/recipe_modules/signing/examples/code_sign.py
@@ -0,0 +1,48 @@
+# Copyright 2023 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import contextlib
+from recipe_engine.post_process import (Filter)
+
+DEPS = [
+    'flutter/signing',
+    'recipe_engine/assertions',
+    'recipe_engine/platform',
+    'recipe_engine/properties',
+    'recipe_engine/raw_io',
+]
+
+
+def RunSteps(api):
+  env = {}
+  env_prefixes = {}
+  files_to_sign = ['file1.zip']
+  if api.properties.get('raises'):
+    with api.assertions.assertRaises(ValueError):
+      api.signing.code_sign(
+          files_to_sign=files_to_sign,
+      )
+  else:
+    api.signing.code_sign(
+        files_to_sign=files_to_sign,
+    )
+
+
+def GenTests(api):
+  yield api.test(
+     'non_mac',
+     api.platform.name('linux'),
+     api.properties(expected_result=False)
+  )
+  yield api.test(
+     'mac_require_signing',
+     api.platform.name('mac'),
+     api.properties(expected_result=True),
+     api.signing.flutter_signing_identity(),
+  )
+  yield api.test(
+     'no_signing_identity',
+     api.platform.name('mac'),
+     api.properties(expected_result=False, raises=True),
+  )
diff --git a/recipe_modules/signing/examples/requires_signing.expected/mac_does_not_require_signing.json b/recipe_modules/signing/examples/requires_signing.expected/mac_does_not_require_signing.json
new file mode 100644
index 0000000..02e3920
--- /dev/null
+++ b/recipe_modules/signing/examples/requires_signing.expected/mac_does_not_require_signing.json
@@ -0,0 +1,69 @@
+[
+  {
+    "cmd": [],
+    "name": "Create test file"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "myfile",
+      "[CLEANUP]/tmp_tmp_1/content/myfile.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@myfile.txt@myfile@@@",
+      "@@@STEP_LOG_END@myfile.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/zip.py"
+    ],
+    "name": "Create test file.create zip",
+    "stdin": "{\"entries\": [{\"path\": \"[CLEANUP]/tmp_tmp_1/content\", \"type\": \"dir\"}], \"output\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\", \"root\": \"[CLEANUP]/tmp_tmp_1/content\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/namelist.py"
+    ],
+    "name": "Create test file.namelist",
+    "stdin": "{\"zip_file\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@json.output (invalid)@@@",
+      "@@@STEP_LOG_LINE@json.output (exception)@No JSON object could be decoded@@@",
+      "@@@STEP_LOG_END@json.output (exception)@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "[CLEANUP]/tmp_tmp_1"
+    ],
+    "infra_step": true,
+    "name": "Create test file.Delete tmp folder",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/requires_signing.expected/mac_require_signing_entitlements.json b/recipe_modules/signing/examples/requires_signing.expected/mac_require_signing_entitlements.json
new file mode 100644
index 0000000..d6b55f4
--- /dev/null
+++ b/recipe_modules/signing/examples/requires_signing.expected/mac_require_signing_entitlements.json
@@ -0,0 +1,107 @@
+[
+  {
+    "cmd": [],
+    "name": "Create test file"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "myfile",
+      "[CLEANUP]/tmp_tmp_1/content/myfile.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@myfile.txt@myfile@@@",
+      "@@@STEP_LOG_END@myfile.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "",
+      "[CLEANUP]/tmp_tmp_1/content/entitlements.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write entitlements.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@entitlements.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "",
+      "[CLEANUP]/tmp_tmp_1/content/without_entitlements.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write without_entitlements.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@without_entitlements.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/zip.py"
+    ],
+    "name": "Create test file.create zip",
+    "stdin": "{\"entries\": [{\"path\": \"[CLEANUP]/tmp_tmp_1/content\", \"type\": \"dir\"}], \"output\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\", \"root\": \"[CLEANUP]/tmp_tmp_1/content\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/namelist.py"
+    ],
+    "name": "Create test file.namelist",
+    "stdin": "{\"zip_file\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"myfile.txt\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"entitlements.txt\"@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "[CLEANUP]/tmp_tmp_1"
+    ],
+    "infra_step": true,
+    "name": "Create test file.Delete tmp folder",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/requires_signing.expected/mac_require_signing_without_entitlements.json b/recipe_modules/signing/examples/requires_signing.expected/mac_require_signing_without_entitlements.json
new file mode 100644
index 0000000..d8d49c7
--- /dev/null
+++ b/recipe_modules/signing/examples/requires_signing.expected/mac_require_signing_without_entitlements.json
@@ -0,0 +1,107 @@
+[
+  {
+    "cmd": [],
+    "name": "Create test file"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "myfile",
+      "[CLEANUP]/tmp_tmp_1/content/myfile.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@myfile.txt@myfile@@@",
+      "@@@STEP_LOG_END@myfile.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "",
+      "[CLEANUP]/tmp_tmp_1/content/entitlements.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write entitlements.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@entitlements.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "",
+      "[CLEANUP]/tmp_tmp_1/content/without_entitlements.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write without_entitlements.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@without_entitlements.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/zip.py"
+    ],
+    "name": "Create test file.create zip",
+    "stdin": "{\"entries\": [{\"path\": \"[CLEANUP]/tmp_tmp_1/content\", \"type\": \"dir\"}], \"output\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\", \"root\": \"[CLEANUP]/tmp_tmp_1/content\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/namelist.py"
+    ],
+    "name": "Create test file.namelist",
+    "stdin": "{\"zip_file\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"myfile.txt\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"without_entitlements.txt\"@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "[CLEANUP]/tmp_tmp_1"
+    ],
+    "infra_step": true,
+    "name": "Create test file.Delete tmp folder",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/requires_signing.expected/non_mac.json b/recipe_modules/signing/examples/requires_signing.expected/non_mac.json
new file mode 100644
index 0000000..88881b6
--- /dev/null
+++ b/recipe_modules/signing/examples/requires_signing.expected/non_mac.json
@@ -0,0 +1,55 @@
+[
+  {
+    "cmd": [],
+    "name": "Create test file"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "myfile",
+      "[CLEANUP]/tmp_tmp_1/content/myfile.txt"
+    ],
+    "infra_step": true,
+    "name": "Create test file.write file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@myfile.txt@myfile@@@",
+      "@@@STEP_LOG_END@myfile.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "RECIPE_MODULE[flutter::zip]/resources/zip.py"
+    ],
+    "name": "Create test file.create zip",
+    "stdin": "{\"entries\": [{\"path\": \"[CLEANUP]/tmp_tmp_1/content\", \"type\": \"dir\"}], \"output\": \"[CLEANUP]/tmp_tmp_1/myzip.zip\", \"root\": \"[CLEANUP]/tmp_tmp_1/content\"}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "[CLEANUP]/tmp_tmp_1"
+    ],
+    "infra_step": true,
+    "name": "Create test file.Delete tmp folder",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/signing/examples/requires_signing.py b/recipe_modules/signing/examples/requires_signing.py
new file mode 100644
index 0000000..b0dbfcf
--- /dev/null
+++ b/recipe_modules/signing/examples/requires_signing.py
@@ -0,0 +1,61 @@
+# Copyright 2023 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import contextlib
+from recipe_engine.post_process import (Filter)
+
+DEPS = [
+    'flutter/signing',
+    'flutter/zip',
+    'recipe_engine/assertions',
+    'recipe_engine/file',
+    'recipe_engine/path',
+    'recipe_engine/platform',
+    'recipe_engine/properties',
+    'recipe_engine/step',
+]
+
+
+@contextlib.contextmanager
+def _create_zip(api, include_entitlements=False):
+  with api.step.nest('Create test file'):
+    directory = api.path.mkdtemp()
+    api.file.write_text('write file', directory.join('content', 'myfile.txt'), 'myfile')
+    if include_entitlements:
+      api.file.write_text('write entitlements.txt', directory.join('content', 'entitlements.txt'), '')
+      api.file.write_text('write without_entitlements.txt', directory.join('content', 'without_entitlements.txt'), '')
+    api.zip.directory('create zip', directory.join('content'), directory.join('myzip.zip'))
+    yield directory.join('myzip.zip')
+    api.file.rmtree('Delete tmp folder', directory)
+
+def RunSteps(api):
+  expected_result = api.properties.get('expected_result')
+  with _create_zip(api, expected_result) as zip_file_name:
+    result = api.signing.requires_signing(zip_file_name)
+    api.assertions.assertEqual(result, expected_result)
+
+
+def GenTests(api):
+  yield api.test(
+     'non_mac',
+     api.platform.name('linux'),
+     api.properties(expected_result=False),
+  )
+  yield api.test(
+     'mac_require_signing_entitlements',
+     api.platform.name('mac'),
+     api.properties(expected_result=True),
+     api.zip.namelist('Create test file.namelist', ['myfile.txt', 'entitlements.txt'])
+  )
+  yield api.test(
+     'mac_require_signing_without_entitlements',
+     api.platform.name('mac'),
+     api.properties(expected_result=True),
+     api.zip.namelist('Create test file.namelist', ['myfile.txt', 'without_entitlements.txt'])
+  )
+  yield api.test(
+     'mac_does_not_require_signing',
+     api.platform.name('mac'),
+     api.properties(expected_result=False),
+  )
diff --git a/recipe_modules/signing/resources/import_certificate.sh b/recipe_modules/signing/resources/import_certificate.sh
new file mode 100644
index 0000000..f4151e1
--- /dev/null
+++ b/recipe_modules/signing/resources/import_certificate.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Helper script to import a flutter p12 identity.
+# Note: do not enable -x to display expanded values of the variables, as this will leak the passwords.
+set -e
+
+RAW_PASSWORD=$(cat $FLUTTER_P12_PASSWORD)
+# Only filepath with a .p12 suffix will be recognized
+mv $FLUTTER_P12 $P12_SUFFIX_FILEPATH
+/usr/bin/security import $P12_SUFFIX_FILEPATH -k build.keychain -P $RAW_PASSWORD -T $CODESIGN_PATH -T /usr/bin/codesign
\ No newline at end of file
diff --git a/recipe_modules/signing/resources/runner.sh b/recipe_modules/signing/resources/runner.sh
new file mode 100644
index 0000000..a35286d
--- /dev/null
+++ b/recipe_modules/signing/resources/runner.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# Helper script to unlock the keychain in the same session
+# as the test runner script.
+set -e
+
+if [ -f /usr/local/bin/unlock_login_keychain.sh ]
+then
+  /usr/local/bin/unlock_login_keychain.sh
+else
+  echo "This bot does not support codesigning"
+fi
\ No newline at end of file
diff --git a/recipe_modules/signing/test_api.py b/recipe_modules/signing/test_api.py
new file mode 100644
index 0000000..ac1fedb
--- /dev/null
+++ b/recipe_modules/signing/test_api.py
@@ -0,0 +1,15 @@
+import os
+import tempfile
+import zipfile
+
+from recipe_engine import recipe_test_api
+
+class RecipeTestingTestApi(recipe_test_api.RecipeTestApi):
+
+  def flutter_signing_identity(self):
+    return self.step_data(
+        'Setup keychain.show-identities',
+        stdout=self.m.raw_io.output_text(
+            '1) ABCD "Developer ID Application: FLUTTER.IO LLC (ABCD)"'
+        )
+    )