Repository: beeware/briefcase

Issue:
Remove the `--no-sign` option and warn the user about `--adhoc-sign` on macOS applications
The `package` command provides `--no-sign` and `--adhoc-sign` options. 

This is only used on macOS. Previously, macOS allowed completely unsigned apps and DMGs. However, as of macOS 12 (Monterey), apps *must* be signed to be able to run at all; and adhoc-signed apps won't be usable on any computer other than the one that created the app. Adhoc signing is useful for local testing, but completely unsigned apps are no longer possible on macOS.

**Describe the solution you'd like**

The `--no-sign` option should be removed. AdHoc signing should become the default signing option for all packaging on all platforms. This should be interpreted as "perform the minimal signing necessary to allow this app to run" - on Windows and Linux, this means "no signing".

Using the `--adhoc-sign` option on macOS should prominently warn the user that the app will work, but will not be re-distributable. 

**Describe alternatives you've considered**

We could retain the `--no-sign` option and make it raise an error; however, that's not a great user experience. As macOS is the only platform that performs signing at present, it makes sense to repurpose "adhoc" signing as "do the minimum legal signing".


Discussion:

Patch:
diff --git a/changes/1214.misc.rst b/changes/1214.misc.rst
new file mode 100644
index 00000000..2abac259
--- /dev/null
+++ b/changes/1214.misc.rst
@@ -0,0 +1,1 @@
+The ``--no-sign`` option for packaging was removed. Briefcase will now prompt for a signing identity during packging, falling back to adhoc/no signing as a default where possible.
diff --git a/docs/reference/commands/package.rst b/docs/reference/commands/package.rst
index 2c3dcb50..cfa08391 100644
--- a/docs/reference/commands/package.rst
+++ b/docs/reference/commands/package.rst
@@ -55,19 +55,20 @@ running:
 ``-p <format>``, ``--packaging-format <format>``
 ------------------------------------------------
 
-The format to use for packaging. The available packaging formats are platform dependent.
-
-``--no-sign``
--------------
-
-Don't perform code signing on the app.
+The format to use for packaging. The available packaging formats are platform
+dependent.
 
 ``--adhoc-sign``
 ----------------
 
-Sign app with adhoc identity.
+Perform the bare minimum signing that will result in a app that can run on your
+local machine. This may result in no signing, or signing with an ad-hoc signing
+identity. The ``--adhoc-sign`` option may be useful during development and
+testing. However, care should be taken using this option for release artefacts,
+as it may not be possible to distribute an ad-hoc signed app to others.
 
 ``-i <identity>`` / ``--identity <identity>``
 ---------------------------------------------
 
-The :doc:`code signing identity </how-to/code-signing/index>` to use when signing the app.
+The :doc:`code signing identity </how-to/code-signing/index>` to use when
+signing the app.
diff --git a/docs/reference/platforms/macOS/app.rst b/docs/reference/platforms/macOS/app.rst
index d35bbfe0..0fac0857 100644
--- a/docs/reference/platforms/macOS/app.rst
+++ b/docs/reference/platforms/macOS/app.rst
@@ -37,3 +37,14 @@ macOS apps.
 Do not submit the application for notarization. By default, apps will be
 submitted for notarization unless they have been signed with an ad-hoc
 signing identity.
+
+Platform quirks
+===============
+
+Packaging with ``--adhoc-sign``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using the ``--adhoc-sign`` option on macOS produces an app that will be able
+to run on your own machine, but won't run on any other computer. In order to
+distribute your app to other users, you will need to sign the app with a full
+signing identity.
diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst
index 3c050656..37a81c17 100644
--- a/docs/reference/platforms/macOS/xcode.rst
+++ b/docs/reference/platforms/macOS/xcode.rst
@@ -43,3 +43,14 @@ macOS apps.
 Do not submit the application for notarization. By default, apps will be
 submitted for notarization unless they have been signed with an ad-hoc
 signing identity.
+
+Platform quirks
+===============
+
+Packaging with ``--adhoc-sign``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using the ``--adhoc-sign`` option on macOS produces an app that will be able
+to run on your own machine, but won't run on any other computer. In order to
+distribute your app to other users, you will need to sign the app with a full
+signing identity.
diff --git a/docs/reference/platforms/windows/app.rst b/docs/reference/platforms/windows/app.rst
index 965ea768..42e4b1ee 100644
--- a/docs/reference/platforms/windows/app.rst
+++ b/docs/reference/platforms/windows/app.rst
@@ -108,3 +108,11 @@ before installing new ones.
 Briefcase will unpack the new support package without cleaning up existing
 support package content. This *should* work; however, ensure a reproducible
 release artefacts, it is advisable to perform a clean app build before release.
+
+Packaging with ``--adhoc-sign``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using the ``--adhoc-sign`` option on Windows results in no signing being
+performed on the packaged app. This will result in your application being
+flagged as coming from an unverified publisher. This may limit who is able to
+install your app.
diff --git a/docs/reference/platforms/windows/visualstudio.rst b/docs/reference/platforms/windows/visualstudio.rst
index c3cf6968..a760884e 100644
--- a/docs/reference/platforms/windows/visualstudio.rst
+++ b/docs/reference/platforms/windows/visualstudio.rst
@@ -132,3 +132,11 @@ before installing new ones.
 Briefcase will unpack the new support package without cleaning up existing
 support package content. This *should* work; however, ensure a reproducible
 release artefacts, it is advisable to perform a clean app build before release.
+
+Packaging with ``--adhoc-sign``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using the ``--adhoc-sign`` option on Windows results in no signing being
+performed on the packaged app. This will result in your application being
+flagged as coming from an unverified publisher. This may limit who can (or is
+willing to) install your app.
diff --git a/src/briefcase/commands/package.py b/src/briefcase/commands/package.py
index cf83e0dd..807290ba 100644
--- a/src/briefcase/commands/package.py
+++ b/src/briefcase/commands/package.py
@@ -10,6 +10,9 @@ class PackageCommand(BaseCommand):
     command = "package"
     description = "Package an app for distribution."
 
+    ADHOC_SIGN_HELP = "Ignored; signing is not supported."
+    IDENTITY_HELP = "Ignored; signing is not supported."
+
     @property
     def packaging_formats(self):
         return [self.output_format]
@@ -115,25 +118,19 @@ class PackageCommand(BaseCommand):
             default=self.default_packaging_format,
             choices=self.packaging_formats,
         )
-        parser.add_argument(
-            "--no-sign",
-            dest="sign_app",
-            help="Disable code signing of the app.",
-            action="store_false",
-        )
-        parser.add_argument(
+
+        # --adhoc-sign and --identity are mutually exclusive
+        signing_group = parser.add_mutually_exclusive_group()
+        signing_group.add_argument(
             "--adhoc-sign",
-            help="Sign the app with adhoc identity.",
+            help=self.ADHOC_SIGN_HELP,
             action="store_true",
         )
-        parser.add_argument(
+        signing_group.add_argument(
             "-i",
             "--identity",
             dest="identity",
-            help=(
-                "The code signing identity to use; either the 40-digit hex "
-                "checksum, or the full name of the identity."
-            ),
+            help=self.IDENTITY_HELP,
             required=False,
         )
 
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index 2a1d671c..13342a4c 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -36,6 +36,10 @@ MACOS_LOG_PREFIX_REGEX = re.compile(
     r"(?P<subsystem>( \(libffi\.dylib\))|( \(_ctypes\.cpython-3\d{1,2}-.*?\.so\)))? (?P<content>.*)"
 )
 
+ADHOC_IDENTITY_NAME = (
+    "Ad-hoc identity. The resulting package will run but cannot be re-distributed."
+)
+
 
 def macOS_log_clean_filter(line):
     """Filter a macOS system log to extract the Python-generated message content.
@@ -225,6 +229,7 @@ class macOSSigningMixin:
         """
         # Obtain the valid codesigning identities.
         identities = self.get_identities(self.tools, "codesigning")
+        identities["-"] = ADHOC_IDENTITY_NAME
 
         if identity:
             try:
@@ -240,22 +245,23 @@ class macOSSigningMixin:
                 except KeyError:
                     # Not found as an ID or name
                     raise BriefcaseCommandError(
-                        f"Invalid code signing identity {identity!r}"
+                        f"Invalid code signing identity {identity}"
                     ) from e
 
-        if len(identities) == 0:
-            raise BriefcaseCommandError(
-                "No code signing identities are available: see "
-                "https://briefcase.readthedocs.io/en/stable/how-to/code-signing/macOS.html"
+        self.input.prompt()
+        self.input.prompt("Select code signing identity to use:")
+        self.input.prompt()
+        identity = select_option(identities, input=self.input)
+        identity_name = identities[identity]
+        if identity == "-":
+            self.logger.info(
+                f"""
+In the future, you could specify this signing identity by running:
+
+    $ briefcase {self.command} macOS --adhoc-sign
+"""
             )
-        elif len(identities) == 1:
-            identity, identity_name = list(identities.items())[0]
         else:
-            self.input.prompt()
-            self.input.prompt("Select code signing identity to use:")
-            self.input.prompt()
-            identity = select_option(identities, input=self.input)
-            identity_name = identities[identity]
             self.logger.info(
                 f"""
 In the future, you could specify this signing identity by running:
@@ -400,6 +406,15 @@ or
 
 
 class macOSPackageMixin(macOSSigningMixin):
+    ADHOC_SIGN_HELP = (
+        "Perform ad-hoc signing on the app. "
+        "The app will only run on this machine; it cannot be redistributed to others."
+    )
+    IDENTITY_HELP = (
+        "The code signing identity to use; either the 40-digit hex "
+        "checksum, or the full name of the identity."
+    )
+
     @property
     def packaging_formats(self):
         return ["app", "dmg"]
@@ -418,8 +433,7 @@ class macOSPackageMixin(macOSSigningMixin):
         super().add_options(parser)
         # We use store_const:False rather than store_false so that the
         # "unspecified" value is None, rather than True, allowing for
-        # a "default behavior" interpretation with `--no-sign` or
-        # `--adhoc-sign` is specified
+        # a "default behavior" interpretation when `--adhoc-sign` is specified
         parser.add_argument(
             "--no-notarize",
             dest="notarize_app",
@@ -456,7 +470,7 @@ class macOSPackageMixin(macOSSigningMixin):
             return re.match(r".*\(([\dA-Z]*)\)", identity_name)[1]
         except TypeError:
             raise BriefcaseCommandError(
-                "Couldn't extract Team ID from signing identity {identity!r}"
+                f"Couldn't extract Team ID from signing identity {identity_name!r}"
             )
 
     def notarize(self, filename, team_id):
@@ -595,7 +609,6 @@ password:
     def package_app(
         self,
         app: BaseConfig,
-        sign_app=True,
         notarize_app=None,
         identity=None,
         adhoc_sign=False,
@@ -604,48 +617,52 @@ password:
         """Package an app bundle.
 
         :param app: The application to package
-        :param sign_app: Should the application be signed? Default: ``True``
         :param notarize_app: Should the app be notarized? Default: ``True`` if the
             app has been signed with a real identity; ``False`` if the app is
             unsigned, or an ad-hoc signing identity has been used.
         :param identity: The code signing identity to use. This can be either
             the 40-digit hex checksum, or the string name of the identity.
             If unspecified, the user will be prompted for a code signing
-            identity. Ignored if ``sign_app`` is ``False``.
-        :param adhoc_sign: If ``True``, code will be signed with adhoc identity of "-"
+            identity. Ignored if ``adhoc_sign`` is ``True``.
+        :param adhoc_sign: If ``True``, code will be signed with ad-hoc identity
+            of "-", and the resulting app will not be re-distributable.
         """
-        if sign_app:
-            if adhoc_sign:
-                if notarize_app:
-                    raise BriefcaseCommandError(
-                        "Can't notarize an app with an adhoc signing identity"
-                    )
-
-                identity = "-"
-                self.logger.info(
-                    "Signing app with adhoc identity...", prefix=app.app_name
-                )
-            else:
-                # If we're signing, and notarization isn't explicitly disabled,
-                # notarize by default.
-                if notarize_app is None:
-                    notarize_app = True
-
-                identity, identity_name = self.select_identity(identity=identity)
+        if adhoc_sign:
+            identity = "-"
+            identity_name = ADHOC_IDENTITY_NAME
+        else:
+            identity, identity_name = self.select_identity(identity=identity)
 
-                self.logger.info(
-                    f"Signing app with identity {identity_name}...", prefix=app.app_name
+        if identity == "-":
+            if notarize_app:
+                raise BriefcaseCommandError(
+                    "Can't notarize an app with an ad-hoc signing identity"
                 )
+            self.logger.info(
+                "Signing app with ad-hoc identity...",
+                prefix=app.app_name,
+            )
+            self.logger.warning(
+                (
+                    "Because you are signing with the ad-hoc identity, this "
+                    "app will run, but cannot be re-distributed."
+                ),
+                prefix=app.app_name,
+            )
+        else:
+            # If we're signing, and notarization isn't explicitly disabled,
+            # notarize by default.
+            if notarize_app is None:
+                notarize_app = True
 
-                if notarize_app:
-                    team_id = self.team_id_from_identity(identity_name)
+            self.logger.info(
+                f"Signing app with identity {identity_name}...", prefix=app.app_name
+            )
 
-            self.sign_app(app=app, identity=identity)
-        else:
             if notarize_app:
-                raise BriefcaseCommandError(
-                    "Can't notarize an app that hasn't been signed"
-                )
+                team_id = self.team_id_from_identity(identity_name)
+
+        self.sign_app(app=app, identity=identity)
 
         if app.packaging_format == "app":
             if notarize_app:
@@ -722,15 +739,14 @@ password:
                 settings=dmg_settings,
             )
 
-            if sign_app:
-                self.sign_file(
-                    dmg_path,
-                    identity=identity,
-                )
+            self.sign_file(
+                dmg_path,
+                identity=identity,
+            )
 
-                if notarize_app:
-                    self.logger.info(
-                        f"Notarizing DMG with team ID {team_id}...",
-                        prefix=app.app_name,
-                    )
-                    self.notarize(dmg_path, team_id=team_id)
+            if notarize_app:
+                self.logger.info(
+                    f"Notarizing DMG with team ID {team_id}...",
+                    prefix=app.app_name,
+                )
+                self.notarize(dmg_path, team_id=team_id)
diff --git a/src/briefcase/platforms/macOS/app.py b/src/briefcase/platforms/macOS/app.py
index edde8749..52d61baf 100644
--- a/src/briefcase/platforms/macOS/app.py
+++ b/src/briefcase/platforms/macOS/app.py
@@ -48,9 +48,9 @@ class macOSAppBuildCommand(macOSAppMixin, macOSSigningMixin, BuildCommand):
         """
         # macOS apps don't have anything to compile, but they do need to be
         # signed to be able to execute on M1 hardware - even if it's only an
-        # adhoc signing identity. Apply an adhoc signing identity to the
+        # ad-hoc signing identity. Apply an ad-hoc signing identity to the
         # app bundle.
-        self.logger.info("Adhoc signing app...", prefix=app.app_name)
+        self.logger.info("Ad-hoc signing app...", prefix=app.app_name)
         self.sign_app(app=app, identity="-")
 
 
diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py
index a9707579..dee57622 100644
--- a/src/briefcase/platforms/windows/__init__.py
+++ b/src/briefcase/platforms/windows/__init__.py
@@ -141,6 +141,13 @@ class WindowsRunCommand(RunCommand):
 
 
 class WindowsPackageCommand(PackageCommand):
+    ADHOC_SIGN_HELP = (
+        "Perform no signing on the app. "
+        "Your app will be reported as coming from an unverified publisher."
+    )
+
+    IDENTITY_HELP = "The 40-digit hex checksum of the code signing identity to use."
+
     @property
     def packaging_formats(self):
         return ["msi", "zip"]
@@ -259,8 +266,8 @@ class WindowsPackageCommand(PackageCommand):
     def package_app(
         self,
         app: AppConfig,
-        sign_app: bool = True,
         identity: str = None,
+        adhoc_sign: bool = False,
         file_digest: str = None,
         use_local_machine: bool = False,
         cert_store: str = None,
@@ -274,8 +281,8 @@ class WindowsPackageCommand(PackageCommand):
         identity is not provided, then ``sign_app=False`` will be enforced.
 
         :param app: The application to package
-        :param sign_app: Should the application be signed? Default: ``True``
         :param identity: SHA-1 thumbprint of the certificate to use for code signing.
+        :param adhoc_sign: Should the application be signed? Default: ``True``
         :param file_digest: File hashing algorithm for code signing.
         :param use_local_machine: True to use cert stores for the Local Machine instead
             of the Current User; default to False for Current User.
@@ -285,8 +292,25 @@ class WindowsPackageCommand(PackageCommand):
         :param timestamp_digest: Hashing algorithm to request from the timestamp server.
         """
 
-        if sign_app and not identity:
+        if adhoc_sign:
+            sign_app = False
+        elif identity:
+            sign_app = True
+        else:
             sign_app = False
+            self.logger.warning(
+                """
+*************************************************************************
+** WARNING: No signing identity provided                               **
+*************************************************************************
+
+    Briefcase will not sign the app. To provide a signing identity,
+    use the `--identity` option; or, to explicitly disable signing,
+    use `--adhoc-sign`.
+
+*************************************************************************
+"""
+            )
 
         if sign_app:
             self.logger.info("Signing App...", prefix=app.app_name)


Test patch:
diff --git a/tests/commands/package/test_call.py b/tests/commands/package/test_call.py
index 812c4297..21f7fd65 100644
--- a/tests/commands/package/test_call.py
+++ b/tests/commands/package/test_call.py
@@ -31,7 +31,6 @@ def test_no_args_package_one_app(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
     ]
@@ -74,7 +73,6 @@ def test_package_one_explicit_app(package_command, first_app, second_app, tmp_pa
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
     ]
@@ -119,7 +117,6 @@ def test_no_args_package_two_app(package_command, first_app, second_app, tmp_pat
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
         # App tools are verified for second app
@@ -131,7 +128,6 @@ def test_no_args_package_two_app(package_command, first_app, second_app, tmp_pat
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 # state of previous calls have been preserved.
                 "package_state": "first",
             },
@@ -146,48 +142,6 @@ def test_no_args_package_two_app(package_command, first_app, second_app, tmp_pat
     assert tmp_path / "base_path" / "dist"
 
 
-def test_no_sign_package_one_app(package_command, first_app, tmp_path):
-    """If there is one app, and a --no-sign argument,package doesn't sign the app."""
-    # Add a single app
-    package_command.apps = {
-        "first": first_app,
-    }
-
-    # Configure a no-sign option
-    options = package_command.parse_options(["--no-sign"])
-
-    # Run the run command
-    package_command(**options)
-
-    # The right sequence of things will be done
-    assert package_command.actions == [
-        # Host OS is verified
-        ("verify-host",),
-        # Tools are verified
-        ("verify-tools",),
-        # App config has been finalized
-        ("finalize-app-config", "first"),
-        # App tools are verified for app
-        ("verify-app-tools", "first"),
-        # Package the first app
-        (
-            "package",
-            "first",
-            {
-                "adhoc_sign": False,
-                "identity": None,
-                "sign_app": False,
-            },
-        ),
-    ]
-
-    # Packaging format has been annotated on the app
-    assert first_app.packaging_format == "pkg"
-
-    # The dist folder has been created.
-    assert tmp_path / "base_path" / "dist"
-
-
 def test_identity_arg_package_one_app(package_command, first_app, tmp_path):
     """If there is one app,and an --identity argument, package signs the app with the
     specified identity."""
@@ -219,7 +173,6 @@ def test_identity_arg_package_one_app(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": "test",
-                "sign_app": True,
             },
         ),
     ]
@@ -232,15 +185,15 @@ def test_identity_arg_package_one_app(package_command, first_app, tmp_path):
 
 
 def test_adhoc_sign_package_one_app(package_command, first_app, tmp_path):
-    """If there is one app,and an --adhoc argument, package signs the app using adhoc
+    """If there is one app,and an --adhoc argument, package signs the app using ad-hoc
     option."""
     # Add a single app
     package_command.apps = {
         "first": first_app,
     }
 
-    # Configure an adhoc signing option
-    options = package_command.parse_options(["--adhoc"])
+    # Configure an ad-hoc signing option
+    options = package_command.parse_options(["--adhoc-sign"])
 
     # Run the run command
     package_command(**options)
@@ -262,7 +215,6 @@ def test_adhoc_sign_package_one_app(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": True,
                 "identity": None,
-                "sign_app": True,
             },
         ),
     ]
@@ -274,71 +226,11 @@ def test_adhoc_sign_package_one_app(package_command, first_app, tmp_path):
     assert tmp_path / "base_path" / "dist"
 
 
-def test_no_sign_args_package_two_app(package_command, first_app, second_app, tmp_path):
-    """If there are multiple apps, and a --no-sign argument,package doesn't sign all the
-    app."""
-    # Add a single app
-    package_command.apps = {
-        "first": first_app,
-        "second": second_app,
-    }
-
-    # Configure a no-sign option
-    options = package_command.parse_options(["--no-sign"])
-
-    # Run the run command
-    package_command(**options)
-
-    # The right sequence of things will be done
-    assert package_command.actions == [
-        # Host OS is verified
-        ("verify-host",),
-        # Tools are verified
-        ("verify-tools",),
-        # App configs have been finalized
-        ("finalize-app-config", "first"),
-        ("finalize-app-config", "second"),
-        # App tools are verified for first app
-        ("verify-app-tools", "first"),
-        # Package the first app
-        (
-            "package",
-            "first",
-            {
-                "adhoc_sign": False,
-                "identity": None,
-                "sign_app": False,
-            },
-        ),
-        # App tools are verified for second app
-        ("verify-app-tools", "second"),
-        # package the second app
-        (
-            "package",
-            "second",
-            {
-                "adhoc_sign": False,
-                "identity": None,
-                "sign_app": False,
-                # state of previous calls have been preserved.
-                "package_state": "first",
-            },
-        ),
-    ]
-
-    # Packaging format has been annotated on both apps
-    assert first_app.packaging_format == "pkg"
-    assert second_app.packaging_format == "pkg"
-
-    # The dist folder has been created.
-    assert tmp_path / "base_path" / "dist"
-
-
 def test_adhoc_sign_args_package_two_app(
     package_command, first_app, second_app, tmp_path
 ):
     """If there are multiple apps,and an --adhoc argument, package signs all apps using
-    adhoc option."""
+    ad-hoc identity."""
 
     package_command.apps = {
         # Add the first app
@@ -348,7 +240,7 @@ def test_adhoc_sign_args_package_two_app(
     }
 
     # Configure adhoc command line options
-    options = package_command.parse_options(["--adhoc"])
+    options = package_command.parse_options(["--adhoc-sign"])
 
     # Run the package command
     package_command(**options)
@@ -371,7 +263,6 @@ def test_adhoc_sign_args_package_two_app(
             {
                 "adhoc_sign": True,
                 "identity": None,
-                "sign_app": True,
             },
         ),
         # App tools are verified for second app
@@ -383,7 +274,6 @@ def test_adhoc_sign_args_package_two_app(
             {
                 "adhoc_sign": True,
                 "identity": None,
-                "sign_app": True,
                 # state of previous calls have been preserved.
                 "package_state": "first",
             },
@@ -433,7 +323,6 @@ def test_identity_sign_args_package_two_app(
             {
                 "adhoc_sign": False,
                 "identity": "test",
-                "sign_app": True,
             },
         ),
         # App tools are verified for second app
@@ -445,7 +334,6 @@ def test_identity_sign_args_package_two_app(
             {
                 "adhoc_sign": False,
                 "identity": "test",
-                "sign_app": True,
                 # state of previous calls have been preserved.
                 "package_state": "first",
             },
@@ -490,7 +378,6 @@ def test_package_alternate_format(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
     ]
@@ -530,7 +417,6 @@ def test_create_before_package(package_command, first_app_config, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
         (
@@ -540,7 +426,6 @@ def test_create_before_package(package_command, first_app_config, tmp_path):
                 "adhoc_sign": False,
                 "create_state": "first",
                 "identity": None,
-                "sign_app": True,
             },
         ),
         # App tools are verified for app
@@ -552,7 +437,6 @@ def test_create_before_package(package_command, first_app_config, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "create_state": "first",
                 "build_state": "first",
             },
@@ -594,7 +478,6 @@ def test_update_package_one_app(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_requirements": True,
                 "update_resources": True,
                 "update_support": True,
@@ -606,7 +489,6 @@ def test_update_package_one_app(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_state": "first",
             },
         ),
@@ -619,7 +501,6 @@ def test_update_package_one_app(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_state": "first",
                 "build_state": "first",
             },
@@ -663,7 +544,6 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_requirements": True,
                 "update_resources": True,
                 "update_support": True,
@@ -675,7 +555,6 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_state": "first",
             },
         ),
@@ -688,7 +567,6 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_state": "first",
                 "build_state": "first",
             },
@@ -700,7 +578,6 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_requirements": True,
                 "update_resources": True,
                 "update_support": True,
@@ -716,7 +593,6 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_state": "second",
                 # state of previous calls have been preserved.
                 "build_state": "first",
@@ -732,7 +608,6 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "update_state": "second",
                 "build_state": "second",
                 # state of previous calls have been preserved.
@@ -777,7 +652,6 @@ def test_build_before_package(package_command, first_app_unbuilt, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
         # App tools are verified for app
@@ -789,7 +663,6 @@ def test_build_before_package(package_command, first_app_unbuilt, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
                 "build_state": "first",
             },
         ),
@@ -836,7 +709,6 @@ def test_already_packaged(package_command, first_app, tmp_path):
             {
                 "adhoc_sign": False,
                 "identity": None,
-                "sign_app": True,
             },
         ),
     ]
diff --git a/tests/platforms/macOS/app/test_build.py b/tests/platforms/macOS/app/test_build.py
index ad0aa2c0..dfd7db05 100644
--- a/tests/platforms/macOS/app/test_build.py
+++ b/tests/platforms/macOS/app/test_build.py
@@ -23,7 +23,7 @@ def build_command(tmp_path):
 
 
 def test_build_app(build_command, first_app_with_binaries):
-    """A macOS App is adhoc signed as part of the build process."""
+    """A macOS App is ad-hoc signed as part of the build process."""
     # Build the app
     build_command.build_app(first_app_with_binaries, test_mode=False)
 
diff --git a/tests/platforms/macOS/app/test_package.py b/tests/platforms/macOS/app/test_package.py
index 06f41145..fd61c8fb 100644
--- a/tests/platforms/macOS/app/test_package.py
+++ b/tests/platforms/macOS/app/test_package.py
@@ -44,7 +44,6 @@ def test_device_option(package_command):
         "identity": None,
         "notarize_app": False,
         "packaging_format": "dmg",
-        "sign_app": True,
         "update": False,
     }
 
@@ -206,34 +205,16 @@ def test_package_app_sign_failure(package_command, first_app_with_binaries, tmp_
     package_command.sign_file.assert_not_called()
 
 
-def test_package_app_no_sign(package_command, first_app_with_binaries):
-    """A macOS App can be packaged without signing or notarization."""
-
-    # Package the app without code signing or notarization
-    package_command.package_app(
-        first_app_with_binaries,
-        sign_app=False,
-        notarize_app=False,
-    )
-
-    # No code signing or notarization has been performed.
-    assert package_command.select_identity.call_count == 0
-    assert package_command.sign_app.call_count == 0
-    assert package_command.sign_file.call_count == 0
-    assert package_command.notarize.call_count == 0
-
-
 def test_package_app_notarize_adhoc_signed(package_command, first_app_with_binaries):
-    """A macOS App cannot be notarized if adhoc signing is requested."""
+    """A macOS App cannot be notarized if ad-hoc signing is requested."""
 
     # Package the app without code signing
     with pytest.raises(
         BriefcaseCommandError,
-        match=r"Can't notarize an app with an adhoc signing identity",
+        match=r"Can't notarize an app with an ad-hoc signing identity",
     ):
         package_command.package_app(
             first_app_with_binaries,
-            sign_app=True,
             notarize_app=True,
             adhoc_sign=True,
         )
@@ -245,51 +226,102 @@ def test_package_app_notarize_adhoc_signed(package_command, first_app_with_binar
     assert package_command.notarize.call_count == 0
 
 
-def test_package_app_notarize_without_sign(package_command, first_app_with_binaries):
-    """A macOS App cannot be notarized if it wasn't signed."""
+def test_package_app_notarize_adhoc_signed_via_prompt(
+    package_command, first_app_with_binaries
+):
+    """A macOS App cannot be notarized if ad-hoc signing is requested."""
 
+    package_command.select_identity.return_value = (
+        "-",
+        (
+            "Ad-hoc identity. The resulting package will run but cannot be "
+            "re-distributed."
+        ),
+    )
     # Package the app without code signing
     with pytest.raises(
         BriefcaseCommandError,
-        match=r"Can't notarize an app that hasn't been signed",
+        match=r"Can't notarize an app with an ad-hoc signing identity",
     ):
         package_command.package_app(
             first_app_with_binaries,
-            sign_app=False,
             notarize_app=True,
         )
 
     # No code signing or notarization has been performed.
-    assert package_command.select_identity.call_count == 0
+    assert package_command.select_identity.call_count == 1
     assert package_command.sign_app.call_count == 0
     assert package_command.sign_file.call_count == 0
     assert package_command.notarize.call_count == 0
 
 
-def test_package_app_notarize_without_sign_default_notarization(
-    package_command, first_app_with_binaries
+def test_package_app_adhoc_signed_via_prompt(
+    package_command, first_app_with_binaries, tmp_path
 ):
-    """A macOS App will default to no notarization if it wasn't signed."""
+    """A macOS App cannot be notarized if ad-hoc signing is requested."""
 
-    # Package the app without code signing; notarization will be disabled
-    # even though it isn't specified
+    package_command.select_identity.return_value = (
+        "-",
+        (
+            "Ad-hoc identity. The resulting package will run but cannot be "
+            "re-distributed."
+        ),
+    )
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
+        notarize_app=False,
     )
 
-    # No code signing or notarization has been performed.
-    assert package_command.select_identity.call_count == 0
-    assert package_command.sign_app.call_count == 0
-    assert package_command.sign_file.call_count == 0
-    assert package_command.notarize.call_count == 0
+    # A request has been made to sign the app
+    package_command.sign_app.assert_called_once_with(
+        app=first_app_with_binaries,
+        identity="-",
+    )
+
+    # The DMG has been built as expected
+    package_command.dmgbuild.build_dmg.assert_called_once_with(
+        filename=os.fsdecode(tmp_path / "base_path" / "dist" / "First App-0.0.1.dmg"),
+        volume_name="First App 0.0.1",
+        settings={
+            "files": [
+                os.fsdecode(
+                    tmp_path
+                    / "base_path"
+                    / "build"
+                    / "first-app"
+                    / "macos"
+                    / "app"
+                    / "First App.app"
+                )
+            ],
+            "symlinks": {"Applications": "/Applications"},
+            "icon_locations": {
+                "First App.app": (75, 75),
+                "Applications": (225, 75),
+            },
+            "window_rect": ((600, 600), (350, 150)),
+            "icon_size": 64,
+            "text_size": 12,
+        },
+    )
+
+    # A request was made to sign the DMG as well.
+    # This ignores the calls that would have been made transitively
+    # by calling sign_app()
+    package_command.sign_file.assert_called_once_with(
+        tmp_path / "base_path" / "dist" / "First App-0.0.1.dmg",
+        identity="-",
+    )
+
+    # No request was made to notarize
+    package_command.notarize.assert_not_called()
 
 
 def test_package_app_adhoc_sign(package_command, first_app_with_binaries, tmp_path):
-    """A macOS App can be packaged and signed with adhoc identity."""
+    """A macOS App can be packaged and signed with ad-hoc identity."""
 
-    # Package the app with an adhoc identity.
-    # Explicitly disable notarization (can't adhoc notarize an app)
+    # Package the app with an ad-hoc identity.
+    # Explicitly disable notarization (can't ad-hoc notarize an app)
     package_command.package_app(
         first_app_with_binaries,
         adhoc_sign=True,
@@ -344,9 +376,9 @@ def test_package_app_adhoc_sign(package_command, first_app_with_binaries, tmp_pa
 def test_package_app_adhoc_sign_default_notarization(
     package_command, first_app_with_binaries, tmp_path
 ):
-    """An adhoc signed app is not notarized by default."""
+    """An ad-hoc signed app is not notarized by default."""
 
-    # Package the app with an adhoc identity; notarization will
+    # Package the app with an ad-hoc identity; notarization will
     # be disabled as a default
     package_command.package_app(
         first_app_with_binaries,
@@ -469,40 +501,6 @@ def test_package_bare_app(package_command, first_app_with_binaries, tmp_path):
         ]
 
 
-def test_package_bare_app_no_sign(package_command, first_app_with_binaries):
-    """A macOS App can be packaged without building dmg, and without signing."""
-    # Select app packaging
-    first_app_with_binaries.packaging_format = "app"
-
-    # Select a code signing identity
-    package_command.select_identity.return_value = (
-        "CAFEBEEF",
-        "Sekrit identity (DEADBEEF)",
-    )
-
-    # Package the app in app (not DMG) format, disabling signing and notarization
-    first_app_with_binaries.packaging_format = "app"
-    package_command.package_app(
-        first_app_with_binaries,
-        sign_app=False,
-        notarize_app=False,
-    )
-
-    # No request has been made to sign the app
-    package_command.sign_app.assert_not_called()
-
-    # No request has been made to notarize the app
-    package_command.notarize.assert_not_called()
-
-    # No dmg was built.
-    assert package_command.dmgbuild.build_dmg.call_count == 0
-
-    # If the DMG doesn't exist, it can't be signed either.
-    # This ignores the calls that would have been made transitively
-    # by calling sign_app()
-    assert package_command.sign_file.call_count == 0
-
-
 def test_package_bare_app_no_notarization(package_command, first_app_with_binaries):
     """A macOS App can be packaged without building dmg, and without notarization."""
     # Select app packaging
@@ -549,8 +547,8 @@ def test_dmg_with_installer_icon(package_command, first_app_with_binaries, tmp_p
     # Package the app without signing or notarization
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
         notarize_app=False,
+        adhoc_sign=True,
     )
 
     # The DMG has been built as expected
@@ -597,8 +595,8 @@ def test_dmg_with_missing_installer_icon(
     # Package the app without signing or notarization
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
         notarize_app=False,
+        adhoc_sign=True,
     )
 
     # The DMG has been built as expected
@@ -649,8 +647,8 @@ def test_dmg_with_app_installer_icon(
     # Package the app without signing or notarization
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
         notarize_app=False,
+        adhoc_sign=True,
     )
 
     # The DMG has been built as expected
@@ -696,8 +694,8 @@ def test_dmg_with_missing_app_installer_icon(
     # Package the app without signing or notarization
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
         notarize_app=False,
+        adhoc_sign=True,
     )
 
     # The DMG has been built as expected
@@ -748,8 +746,8 @@ def test_dmg_with_installer_background(
     # Package the app without signing or notarization
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
         notarize_app=False,
+        adhoc_sign=True,
     )
 
     # The DMG has been built as expected
@@ -796,8 +794,8 @@ def test_dmg_with_missing_installer_background(
     # Package the app without signing or notarization
     package_command.package_app(
         first_app_with_binaries,
-        sign_app=False,
         notarize_app=False,
+        adhoc_sign=True,
     )
 
     # The DMG has been built as expected
diff --git a/tests/platforms/macOS/app/test_signing.py b/tests/platforms/macOS/app/test_signing.py
index 4a5c7d02..75301b9f 100644
--- a/tests/platforms/macOS/app/test_signing.py
+++ b/tests/platforms/macOS/app/test_signing.py
@@ -167,37 +167,48 @@ def test_invalid_identity_name(dummy_command):
 
 
 def test_implied_identity(dummy_command):
-    """If there is only one identity, it is automatically picked."""
+    """If there is only one identity, it will still prompt with ad-hoc as a second
+    option."""
     # get_identities will return some options.
     dummy_command.get_identities.return_value = {
         "11E77FB58F13F6108B38110D5D92233C58ED38C5": "iPhone Developer: Jane Smith (BXAH5H869S)",
     }
 
+    # Return option 2
+    dummy_command.input.values = ["2"]
+
     result = dummy_command.select_identity()
 
-    # The identity will be the only option available.
     assert result == (
         "11E77FB58F13F6108B38110D5D92233C58ED38C5",
         "iPhone Developer: Jane Smith (BXAH5H869S)",
     )
 
-    # User input was not solicited
-    assert dummy_command.input.prompts == []
+    # User input was solicited
+    assert dummy_command.input.prompts
 
 
 def test_no_identities(dummy_command):
-    """If there are no identities an error is raised."""
+    """If there are no identities the user will be prompted to select with only ad-hoc
+    as an option."""
     # get_identities will return some options.
     dummy_command.get_identities.return_value = {}
 
-    with pytest.raises(
-        BriefcaseCommandError,
-        match=(
-            r"No code signing identities are available: see "
-            r"https://briefcase.readthedocs.io/en/stable/how-to/code-signing/macOS.html"
+    # Return option 1
+    dummy_command.input.values = ["1"]
+
+    result = dummy_command.select_identity()
+
+    assert result == (
+        "-",
+        (
+            "Ad-hoc identity. The resulting package will run but cannot be "
+            "re-distributed."
         ),
-    ):
-        dummy_command.select_identity()
+    )
+
+    # User input was solicited
+    assert dummy_command.input.prompts
 
 
 def test_selected_identity(dummy_command):
@@ -208,8 +219,8 @@ def test_selected_identity(dummy_command):
         "11E77FB58F13F6108B38110D5D92233C58ED38C5": "iPhone Developer: Jane Smith (BXAH5H869S)",
     }
 
-    # Return option 2
-    dummy_command.input.values = ["2"]
+    # Return option 3
+    dummy_command.input.values = ["3"]
 
     result = dummy_command.select_identity()
 
@@ -224,8 +235,8 @@ def test_selected_identity(dummy_command):
 
 
 def test_sign_file_adhoc_identity(dummy_command, tmp_path):
-    """If an adhoc identity is used, the runtime option isn't used."""
-    # Sign the file with an adhoc identity
+    """If an ad-hoc identity is used, the runtime option isn't used."""
+    # Sign the file with an ad-hoc identity
     dummy_command.sign_file(tmp_path / "base_path" / "random.file", identity="-")
 
     # An attempt to codesign was made without the runtime option
@@ -245,7 +256,7 @@ def test_sign_file_adhoc_identity(dummy_command, tmp_path):
 
 def test_sign_file_entitlements(dummy_command, tmp_path):
     """Entitlements can be included in a signing call."""
-    # Sign the file with an adhoc identity
+    # Sign the file with an ad-hoc identity
     dummy_command.sign_file(
         tmp_path / "base_path" / "random.file",
         identity="Sekrit identity (DEADBEEF)",
diff --git a/tests/platforms/windows/app/test_package.py b/tests/platforms/windows/app/test_package.py
index 3bf70832..fe08435e 100644
--- a/tests/platforms/windows/app/test_package.py
+++ b/tests/platforms/windows/app/test_package.py
@@ -94,6 +94,7 @@ def test_verify_with_signing(package_command):
     "cli_args, signing_options, is_sdk_needed",
     [
         ([], {}, False),
+        (["--adhoc-sign"], dict(adhoc_sign=True), False),
         (["--file-digest", "sha2000"], dict(file_digest="sha2000"), False),
         (["-i", "asdf"], dict(identity="asdf"), True),
         (["--identity", "asdf"], dict(identity="asdf"), True),
@@ -158,7 +159,6 @@ def test_parse_options(package_command, cli_args, signing_options, is_sdk_needed
         timestamp_digest="sha256",
         adhoc_sign=False,
         packaging_format="msi",
-        sign_app=True,
         update=False,
     )
     expected_options = {**default_options, **signing_options}
@@ -169,10 +169,17 @@ def test_parse_options(package_command, cli_args, signing_options, is_sdk_needed
     assert package_command._windows_sdk_needed is is_sdk_needed
 
 
-def test_package_msi(package_command, first_app_config, tmp_path):
+@pytest.mark.parametrize(
+    "kwargs",
+    [
+        dict(),  # Default behavior (adhoc signing)
+        {"adhoc_sign": True},  # Explicit adhoc signing
+    ],
+)
+def test_package_msi(package_command, first_app_config, kwargs, tmp_path):
     """A Windows app can be packaged as an MSI."""
 
-    package_command.package_app(first_app_config)
+    package_command.package_app(first_app_config, **kwargs)
 
     assert package_command.tools.subprocess.run.mock_calls == [
         # Collect manifest
@@ -239,11 +246,21 @@ def test_package_msi(package_command, first_app_config, tmp_path):
     ]
 
 
-def test_package_zip(package_command_with_files, first_app_config, tmp_path):
+@pytest.mark.parametrize(
+    "kwargs",
+    [
+        dict(),  # Default behavior (adhoc signing)
+        {"adhoc_sign": True},  # Explicit adhoc signing
+    ],
+)
+def test_package_zip(package_command_with_files, first_app_config, kwargs, tmp_path):
     """A Windows app can be packaged as a zip file."""
 
     first_app_config.packaging_format = "zip"
-    package_command_with_files.package_app(first_app_config)
+    package_command_with_files.package_app(first_app_config, **kwargs)
+
+    # No signing was performed
+    assert package_command_with_files.tools.subprocess.run.mock_calls == []
 
     archive_file = tmp_path / "base_path" / "dist" / "First App-0.0.1.zip"
     source_folders_and_files = (