| #!/usr/bin/env python3 |
| # Copyright (C) 2020 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Helper tool for signing repo launcher scripts correctly. |
| |
| This is intended to be run only by the official Repo release managers. |
| """ |
| |
| import argparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| import util |
| |
| |
| def sign(opts): |
| """Sign the launcher!""" |
| output = "" |
| for key in opts.keys: |
| # We use ! at the end of the key so that gpg uses this specific key. |
| # Otherwise it uses the key as a lookup into the overall key and uses |
| # the default signing key. i.e. It will see that KEYID_RSA is a subkey |
| # of another key, and use the primary key to sign instead of the subkey. |
| cmd = [ |
| "gpg", |
| "--homedir", |
| opts.gpgdir, |
| "-u", |
| f"{key}!", |
| "--batch", |
| "--yes", |
| "--armor", |
| "--detach-sign", |
| "--output", |
| "-", |
| opts.launcher, |
| ] |
| ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE) |
| output += ret.stdout |
| |
| # Save the combined signatures into one file. |
| with open(f"{opts.launcher}.asc", "w", encoding="utf-8") as fp: |
| fp.write(output) |
| |
| |
| def check(opts): |
| """Check the signature.""" |
| util.run(opts, ["gpg", "--verify", f"{opts.launcher}.asc"]) |
| |
| |
| def get_version(opts): |
| """Get the version from |launcher|.""" |
| # Make sure we don't search $PATH when signing the "repo" file in the cwd. |
| launcher = os.path.join(".", opts.launcher) |
| cmd = [launcher, "--version"] |
| ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE) |
| m = re.search(r"repo launcher version ([0-9.]+)", ret.stdout) |
| if not m: |
| sys.exit(f"{opts.launcher}: unable to detect repo version") |
| return m.group(1) |
| |
| |
| def postmsg(opts, version): |
| """Helpful info to show at the end for release manager.""" |
| print( |
| f""" |
| Repo launcher bucket: |
| gs://git-repo-downloads/ |
| |
| You should first upload it with a specific version: |
| gsutil cp -a public-read {opts.launcher} gs://git-repo-downloads/repo-{version} |
| gsutil cp -a public-read {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc |
| |
| Then to make it the public default: |
| gsutil cp -a public-read gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo |
| gsutil cp -a public-read gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc |
| |
| NB: If a rollback is necessary, the GS bucket archives old versions, and may be |
| accessed by specifying their unique id number. |
| gsutil ls -la gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc |
| gsutil cp -a public-read gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo |
| gsutil cp -a public-read gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc |
| """ # noqa: E501 |
| ) |
| |
| |
| def get_parser(): |
| """Get a CLI parser.""" |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "-n", |
| "--dry-run", |
| dest="dryrun", |
| action="store_true", |
| help="show everything that would be done", |
| ) |
| parser.add_argument( |
| "--gpgdir", |
| default=os.path.join(util.HOMEDIR, ".gnupg", "repo"), |
| help="path to dedicated gpg dir with release keys " |
| "(default: ~/.gnupg/repo/)", |
| ) |
| parser.add_argument( |
| "--keyid", |
| dest="keys", |
| default=[], |
| action="append", |
| help="alternative signing keys to use", |
| ) |
| parser.add_argument( |
| "launcher", |
| default=os.path.join(util.TOPDIR, "repo"), |
| nargs="?", |
| help="the launcher script to sign", |
| ) |
| return parser |
| |
| |
| def main(argv): |
| """The main func!""" |
| parser = get_parser() |
| opts = parser.parse_args(argv) |
| |
| if not os.path.exists(opts.gpgdir): |
| parser.error(f"--gpgdir does not exist: {opts.gpgdir}") |
| if not os.path.exists(opts.launcher): |
| parser.error(f"launcher does not exist: {opts.launcher}") |
| |
| opts.launcher = os.path.relpath(opts.launcher) |
| print( |
| f'Signing "{opts.launcher}" launcher script and saving to ' |
| f'"{opts.launcher}.asc"' |
| ) |
| |
| if opts.keys: |
| print(f'Using custom keys to sign: {" ".join(opts.keys)}') |
| else: |
| print("Using official Repo release keys to sign") |
| opts.keys = [util.KEYID_DSA, util.KEYID_RSA, util.KEYID_ECC] |
| util.import_release_key(opts) |
| |
| version = get_version(opts) |
| sign(opts) |
| check(opts) |
| postmsg(opts, version) |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |