Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support azure signtool #771

Merged
merged 23 commits into from
May 8, 2024
Merged

Conversation

marcoesters
Copy link
Contributor

Description

Add support for AzureSignTool to sign Windows installers. AzureSignTool uses Azure key vaults, which do not use certificate files on disk. To achieve this, this PR:

  • Adds a windows_signing_tool keyword that selects the tool to sign. This key determines whether to sign the installer instead of signing_certificate, which makes signing more extensible.
  • Creates classes for Windows signing tools.
  • Adds documentation for all environment variables for both SignTool and AzureSignTool.
  • Allows signtool.exe to use a different digest algorithms.

Since this requires access to Azure, tests cannot be added for this feature.

Closes #767.

Checklist - did you ...

  • Add a file to the news directory (using the template) for the next release's release notes?
  • Add / update necessary tests?
  • Add / update outdated documentation?

@conda-bot conda-bot added the cla-signed [bot] added once the contributor has signed the CLA label Apr 9, 2024
@marcoesters marcoesters marked this pull request as ready for review April 9, 2024 17:51
@marcoesters marcoesters requested a review from a team as a code owner April 9, 2024 17:51
docs/source/howto.md Outdated Show resolved Hide resolved
docs/source/howto.md Outdated Show resolved Hide resolved
docs/source/howto.md Outdated Show resolved Hide resolved
@@ -109,6 +109,12 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
if info.get(key): # only join if there's a truthy value set
info[key] = abspath(join(dir_path, info[key]))

# Normalize name and set default value
if info.get("windows_signing_tool"):
info["windows_signing_tool"] = info["windows_signing_tool"].lower().replace(".exe", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a user tries to set this to a full path? Might be tempting 😬

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the same time, if we are validating the input in construct.py:805, why do we need normalization here? 🤔 For library usage? In that case it should be the same logic (or move the logic here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a user tries to set this to a full path?

constructor will and should fail. windows_signing_tool is the name. Otherwise, I would have to infer from the binary what the appropriate signing too is.

In that case it should be the same logic

Yes, that's true, good catch!

(or move the logic here).

I'm not sure about this. Setting default values and manipulating input is done in main_build. validate() performs validation here. If we move this line into validate(), we are mixing scopes, which makes things harder to track.

constructor/signing.py Outdated Show resolved Hide resolved
constructor/signing.py Outdated Show resolved Hide resolved
constructor/signing.py Outdated Show resolved Hide resolved
constructor/signing.py Outdated Show resolved Hide resolved
constructor/winexe.py Outdated Show resolved Hide resolved
Copy link
Contributor

@jaimergp jaimergp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work. I'm a little concerned about the lack of tests, so I wonder if it's feasible to have a some secrets added to a dummy vault (or something that implements the same API). However, that might be too complicated and this feature will be used routinely by Anaconda anyway, so I guess we'll know when it breaks.

Double check that the docs render correctly locally, I found a little typo with the admonition syntax. Other than that, this is good to go after addressing the comments.

@marcoesters
Copy link
Contributor Author

so I wonder if it's feasible to have a some secrets added to a dummy vault (or something that implements the same API).

I will work internally on this and will see that I can get tests into this PR.

@marcoesters
Copy link
Contributor Author

I added integration tests and secrets were added to the repository. However, the secrets will not be available for anybody who submits a PR from the fork, which will cause the tests to fail. The tests do execute the correct command though: https://github.com/conda/constructor/actions/runs/8760110611/job/24044501617#step:9:2133

Those tests will have to be skipped until merged into main. So, we will have to add
As discussed with @jaimergp offline, the best way to go here is to add test failure reporting as they do for conda-libmambda-solver: https://github.com/conda/conda-libmamba-solver/blob/efa3b84141e4ff777928aaae952fac417b059091/.github/workflows/tests.yml#L680-L690

This requires a token, so I will convert this PR into draft until that infrastructure is in place.

Additional things I found:

  • The environment is not copied into the subprocess on Windows when no extra environment variables are set. On POSIX, this is handled by the subprocess module, but not for Windows. So, I copy the environment explicitly for all to get conistent behavior.
  • The debugging section was not in any TOC tree for the documentations, so I added it.

)
command = (
f"{win_str_esc(self.executable)} sign /f {win_str_esc(self.certificate_file)} "
f"/tr {win_str_esc(timestamp_server)} /td {timestamp_digest} /fd {file_digest}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting here that no quotes are needed because we don't expect spaces here.

command = (
f"{self.executable} sign -v"
' -kvu %AZURE_SIGNTOOL_KEY_VAULT_URL%'
' -kvc %AZURE_SIGNTOOL_KEY_VAULT_CERTIFICATE%'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we quote this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can. I just tested it locally and the test worked fine with quotes, too.

Comment on lines 166 to 168
" -kvi %AZURE_SIGNTOOL_KEY_VAULT_CLIENT_ID%"
" -kvt %AZURE_SIGNTOOL_KEY_VAULT_TENANT_ID%"
" -kvs %AZURE_SIGNTOOL_KEY_VAULT_SECRET%"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, do we need to quote any of these?

raise RuntimeError(f"Signature verification failed.\n{proc.stderr}")
try:
status, status_message = proc.stdout.strip().split("\n")
status = int(status)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this fail if status is empty or None?

Copy link
Contributor Author

@marcoesters marcoesters Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how this could be None unless proc.stdout can somehow be None.

If status is an empty string, int(status) raises a ValueError, which is caught further down.

)
@pytest.mark.parametrize(
"auth_method",
os.environ.get("AZURE_SIGNTOOL_TEST_AUTH_METHODS", "token,secret").split(","),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this "parametrized" parametrization? Are you using it locally while debugging? I don't see it in the CI setup 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make sure that all authentication methods could potentially be covered. For tokens and secrets, I can just check the environment variables and skip if needed. For managed identities, however, this does not work because there isn't an environment variable to check for (logging into the vault must happen before AzureSignTool is executed).

So, the only "safe" methods to add into the parametrization are token and secret. However, I still wanted to allow testing with a managed identity.

Does that make sense? I tried to explain this briefly in the docstring.

@marcoesters marcoesters marked this pull request as ready for review May 1, 2024 22:25
"""Test signing installers with AzureSignTool.

There are three ways to authenticate with Azure: tokens, secrets, and managed identities.
There is no good sentinel environment for manged identities, so an environment variable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
There is no good sentinel environment for manged identities, so an environment variable
There is no good sentinel environment for managed identities, so an environment variable

I think?


If neither `AZURE_SIGNTOOL_KEY_VAULT_ACCESSTOKEN` nor `AZURE_SIGNTOOL_KEY_VAULT_SECRET` are set, `constructor` will use a Managed Identity (`-kvm` CLI option).
:::
### Signing and notarizing PKG installers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Signing and notarizing PKG installers
### Signing and notarizing PKG installers

@marcoesters marcoesters merged commit a838162 into conda:main May 8, 2024
17 checks passed
@marcoesters marcoesters deleted the support-azure-signtool branch May 8, 2024 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla-signed [bot] added once the contributor has signed the CLA
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Support AzureSignTool
3 participants