diff --git a/Atomic/pull.py b/Atomic/pull.py index 18a46fa..c195258 100644 --- a/Atomic/pull.py +++ b/Atomic/pull.py @@ -2,7 +2,7 @@ try: from . import Atomic except ImportError: from atomic import Atomic # pylint: disable=relative-import -from .util import skopeo_copy, get_atomic_config, skopeo_inspect, decompose, write_out +from .util import skopeo_copy, get_atomic_config, get_atomic_config_item, skopeo_inspect, decompose, write_out, write_registry_config, install_pubkey, update_trust_policy ATOMIC_CONFIG = get_atomic_config() @@ -27,6 +27,9 @@ class Pull(Atomic): tag = tag if tag != "" else "latest" fq_name = skopeo_inspect("docker://{}".format(self.args.image))['Name'] image = "docker-daemon:{}:{}".format(fq_name, tag) + if get_atomic_config_item(['discover_sigstores'], get_atomic_config()): + if not self.discover_sigstore(): + write_out("There was a problem configuring the trust policy") skopeo_copy("docker://{}".format(self.args.image), image, debug=self.args.debug) def pull_image(self): @@ -41,3 +44,72 @@ class Pull(Atomic): write_out("Image %s is being pulled to %s ..." % (self.args.image, self.args.backend)) handler() + def discover_sigstore(self): + """ + Check for registry/repo/sigstore metadata image + prompt user for trust on first use workflow + :return: True if sigstore discovered and configured + """ + (registry, repo, _) = decompose(self.args.image) + # FIXME: this should be handled in util.decompose + _repo, _image = repo.split('/') + # TODO: check local /etc/containers/registries.d config here + repo_sigstore_labels = self._get_sigstore_image_metadata(registry, _repo) + if self._validate_sigstore_labels(repo_sigstore_labels): + if self._prompt_trust(repo_sigstore_labels): + discover_config = False + trust_scope = "%s/%s" % (registry, _repo) + discover_config = write_registry_config(trust_scope) + pubkey_path = install_pubkey(repo_sigstore_labels['pubkey-id'], repo_sigstore_labels['pubkey-url']) + discover_config = update_trust_policy(trust_scope, pubkey_path, repo_sigstore_labels['sigstore-url']) + return discover_config + return False + + def _get_sigstore_image_metadata(self, registry, repo): + """ + Get sigstore metadata image + :param registry: registry string + :param repo: repo string + :return True on success + """ + _img = get_atomic_config_item(['sigstore_metadata_image'], get_atomic_config()) + sigstoreimage = '/'.join([registry, repo, _img]) + data = skopeo_inspect("docker://" + sigstoreimage, args=None, fail_silent=True) + if data: + write_out("Found registry sigstore metadata image %s" % sigstoreimage) + return data['Labels'] + else: + return False + + def _validate_sigstore_labels(self, labels): + """ + Validate sigstore metadata. + If there's a missing key or something we don't want to perform any automatic trust policy configuration + :param labels: unvalidated labels. Should be either dict or False + :return: True if labels are valid + """ + valid = False + if labels: + expected_keys = ["pubkey-id", "pubkey-fingerprint", "pubkey-url", "sigstore-url"] + for k in expected_keys: + valid = k in labels + return valid + + def _prompt_trust(self, labels): + """ + Prompt user for trust on first use workflow + :param labels: dict of metadata labels defining sigstore trust + :return: True if user accepts + """ + write_out("ID: " + labels['pubkey-id']) + write_out("Fingerprint: " + labels['pubkey-fingerprint']) + write_out("Public key download URL: %s" % labels['pubkey-url']) + confirm = None + if self.args.assumeyes: + confirm = "yes" + else: + confirm = util.input("Do you want to add trust policy for this registry? (y/N)") + if not "y" in confirm.lower(): + return False + return True + diff --git a/Atomic/util.py b/Atomic/util.py index e75644e..08e0f8a 100644 --- a/Atomic/util.py +++ b/Atomic/util.py @@ -52,7 +52,9 @@ def check_if_python2(): input, is_python2 = check_if_python2() # pylint: disable=redefined-builtin def decompose(compound_name): - # '[reg/]repo[:tag]' -> (reg, repo, tag) + # TODO: this doesn't behave when the registry is omitted or using hub "library" images + # we should really decompose into reg, repo, image and tag components + # 'reg/repo/image[:tag]' -> (reg, repo, image, tag) reg, repo, tag = '', compound_name, '' if '/' in repo: reg, repo = repo.split('/', 1) @@ -245,14 +247,15 @@ def urllib3_disable_warnings(): if hasattr(urllib3, 'disable_warnings'): urllib3.disable_warnings() -def skopeo_inspect(image, args=None, return_json=True, newline=False): +def skopeo_inspect(image, args=None, return_json=True, newline=False, quiet=False): if not args: args=[] # Performs remote inspection of an image on a registry # :param image: fully qualified name # :param args: additional parameters to pass to Skopeo - # :return: Returns json formatted data + # :param fail_silent: return false if failed + # :return: Returns json formatted data or false # Adding in --verify-tls=false to deal with the change in skopeo # policy. The prior inspections were also false. We need to define @@ -266,6 +269,8 @@ def skopeo_inspect(image, args=None, return_json=True, newline=False): except OSError: raise ValueError("skopeo must be installed to perform remote inspections") if results.return_code is not 0: + if quiet: + return False raise ValueError(results) else: if return_json: @@ -368,6 +373,53 @@ def get_atomic_config(atomic_config=None): with open(atomic_config, 'r') as conf_file: return yaml_load(conf_file) +def write_registry_config(scope): + """ + Write registry sigstore configuration file + :param scope: registry string + :return: True on success + """ + # FIXME: pending agreement on registry sigstore layout + registry_dir = get_atomic_config_item(['registry_sigstore_dir'], get_atomic_config()) + write_out("TODO: Writing trust config for %s to %s" % (scope, registry_dir)) + return False + +def install_pubkey(key_name, key_url): + """ + Installs public key to system config directory + :param key_name: id of key used as filename + :param key_url: download URI of public key + :return: pubkey path string or False + """ + pubkeys_dir = get_atomic_config_item(['pubkeys_dir'], get_atomic_config()) + pubkey_file = "%s/%s" % (pubkeys_dir, key_name) + if not os.path.exists(pubkeys_dir): + os.mkdir(pubkeys_dir) + if os.path.exists(pubkey_file): + write_out("Public key %s already installed at %s" % (key_name, pubkey_file)) + else: + r = requests.get(key_url) + if r.status_code == 200: + with open(pubkey_file, 'w') as pubfile: + pubfile.write(r.content) + write_out("Installed public key %s" % pubkey_file) + else: + write_out("WARNING: Could not download public key using URL %s." % key_url) + write_out("Download the public key manually and install as %s" % pubkey_file) + return pubkey_file + +def update_trust_policy(trust_scope, pubkey_path, sigstore_url): + """ + Add trust policy for the specified registry scope + :param trust_scope: registry/repository scope + :param pubkey_path: absolute public key path + :param sigstore_url: url of sigstore + :return: True if success + """ + # FIXME: pending feedback on manage policy + write_out("TODO: Adding trust policy: %s %s %s" % (trust_scope, pubkey_path, sigstore_url)) + return False + def add_opt(sub): sub.add_argument("--opt1", dest="opt1",help=argparse.SUPPRESS) sub.add_argument("--opt2", dest="opt2",help=argparse.SUPPRESS) diff --git a/atomic.conf b/atomic.conf index 11b7369..a952cdf 100644 --- a/atomic.conf +++ b/atomic.conf @@ -3,6 +3,9 @@ default_scanner: default_docker: docker registry_confdir: /etc/containers/registries.d/ +discover_sigstores: true +sigstore_metadata_image: sigstore +pubkeys_dir: /etc/pki/containers # Default storage backend [ostree, docker]