#!/usr/bin/perl -w
#
# Copyright (c) 2018 SUSE Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
#
# Registry interfacing
#

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC,  "$wd/build";
  unshift @INC,  "$wd";
}

use JSON::XS ();
use Digest::SHA ();
use Data::Dumper;
use Compress::Zlib;

use BSRPC ':https';
use BSHTTP;
use BSTar;
use BSContar;
use BSUtil;
use BSBearer;

use strict;

my $registry_timeout = 300;

my $dest_creds;
my $use_image_tags;
my $multiarch;
my $digestfile;
my $writeinfofile;
my $delete_mode;
my $delete_except_mode;
my $list_mode;
my $no_cosign_info;
my @tags;
my $blobdir;
my $oci;

my $cosign;
my $cosigncookie;
my $slsaprovenance;
my $sbom;
my $gun;
my @signcmd;
my $pubkeyfile;

my $rekorserver;

my $registryserver;
my $repository;
my @tarfiles;

my $registry_authenticator;
my $keepalive;

my $cosign_cookie_name = 'org.open-build-service.cosign.cookie';

sub send_layer {
  my ($param) = @_;
  my $sl = $param->{'send_layer_data'}; # [ $layer_ent, $offset ],
  my $chunk = BSTar::extract($sl->[0]->{'file'}, $sl->[0], $sl->[1], 65536);
  $sl->[1] += length($chunk);
  return $chunk;
}

sub blob_exists {
  my ($blobid, $size) = @_;
  my $replyheaders;
  my $param = {
    'uri' => "$registryserver/v2/$repository/blobs/$blobid",
    'request' => 'HEAD',
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
    'replyheaders' => \$replyheaders,
    'maxredirects' => 1,
  };
  eval { BSRPC::rpc($param) };
  if ($replyheaders) {
    die("size mismatch?\n") if $replyheaders->{'content-length'} && $size && $size != $replyheaders->{'content-length'};
    return 0 if $replyheaders->{'docker-content-digest'} && $blobid ne $replyheaders->{'docker-content-digest'};
    return 1;
  }
  return 0;
}

sub blob_upload {
  my ($blobid, $upload_ent) = @_;

  my $size = $upload_ent->{'size'};
  return $blobid if blob_exists($blobid, $size);
  print "uploading layer $blobid... ";
  my $replyheaders;
  my $param = {
    'uri' => "$registryserver/v2/$repository/blobs/uploads/",
    'request' => 'POST',
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
    'replyheaders' => \$replyheaders,
  };
  BSRPC::rpc($param);
  my $loc = $replyheaders->{'location'};
  my @locextra;
  if ($loc =~ s/\?(.*)$//) {
    push @locextra, split('&', $1);
    s/%([a-fA-F0-9]{2})/chr(hex($1))/ge for @locextra;
  }
  die("no location in upload reply\n") unless $loc;
  $loc = "$registryserver$loc" if $loc =~ /^\//;
  $param = {
    'headers' => [ "Content-Length: $size", "Content-Type: application/octet-stream" ],
    'uri' => $loc,
    'request' => 'PUT',
    'authenticator' => $registry_authenticator,
    'replyheaders' => \$replyheaders,
    'data' => \&send_layer,
    'send_layer_data' => [ $upload_ent, 0 ],
  };
  $replyheaders = undef;
  BSRPC::rpc($param, undef, @locextra, "digest=$blobid");
  my $id = $replyheaders->{'docker-content-digest'};
  die("server did not return a content id\n") unless $id;
  die("server created a different blobid: $blobid $id\n") if $blobid ne $id;
  print "done.\n";
  return $blobid;
}

sub blob_download {
  my ($blobid, $filename) = @_;
  my $stdout_receiver = sub {
    while(1) {
      my $s = BSHTTP::read_data($_[0], 8192);
      return {} if $s eq '';
      print($s) || die("write: $!\n");
    }
  };
  my $param = {
    'uri' => "$registryserver/v2/$repository/blobs/$blobid",
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
    'receiver' => $filename eq '-' ? $stdout_receiver : \&BSHTTP::file_receiver,
    'filename' => $filename,
    'maxredirects' => 1,
  };
  BSRPC::rpc($param);
}

sub blob_fetch {
  my ($blobid) = @_;
  my $param = {
    'uri' => "$registryserver/v2/$repository/blobs/$blobid",
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
    'maxredirects' => 1,
  };
  return BSRPC::rpc($param);
}

sub manifest_exists {
  my ($manifest, $tag, $content_type) = @_;

  $content_type ||= $BSContar::mt_docker_manifest;
  my $maniid = BSContar::blobid($manifest);
  $tag = $maniid unless defined $tag;
  my $replyheaders;
  my $param = {
    'headers' => [ "Accept: $content_type" ],
    'uri' => "$registryserver/v2/$repository/manifests/$tag",
    'authenticator' => $registry_authenticator,
    'replyheaders' => \$replyheaders,
    'timeout' => $registry_timeout,
    'keepalive' => $keepalive,
  };
  my $maniret;
  eval { $maniret = BSRPC::rpc($param) };
  if ($maniret) {
    my $maniretid = $replyheaders->{'docker-content-digest'};
    $maniretid ||= BSContar::blobid($maniret);
    return 1 if $maniretid eq $maniid;
  }
  return 0;
}

sub manifest_upload {
  my ($manifest, $tag, $content_type, $quiet) = @_;

  my $maniid = BSContar::blobid($manifest);
  return $maniid if manifest_exists($manifest, $tag, $content_type);
  $content_type ||= $BSContar::mt_docker_manifest;
  my $fat = '';
  $fat = 'fat ' if $content_type eq $BSContar::mt_docker_manifestlist || $content_type eq $BSContar::mt_oci_index;
  if (!$quiet) {
    if (defined($tag)) {
      print "uploading ${fat}manifest $maniid for tag '$tag'... ";
    } else {
      print "uploading ${fat}manifest $maniid... ";
    }
  }
  $tag = $maniid unless defined $tag;
  my $replyheaders;
  my $param = {
    'headers' => [ "Content-Type: $content_type" ],
    'uri' => "$registryserver/v2/$repository/manifests/$tag",
    'request' => 'PUT',
    'authenticator' => $registry_authenticator,
    'replyheaders' => \$replyheaders,
    'data' => $manifest,
  };
  BSRPC::rpc($param, undef);
  my $id = $replyheaders->{'docker-content-digest'};
  die("server did not return a content id\n") unless $id;
  die("server created a different manifest id: $maniid $id\n") if $maniid ne $id;
  print "done.\n" unless $quiet;
  return $maniid;
}

sub manifest_append {
  my ($manifest, $tag) = @_;
  my $maniid = BSContar::blobid($manifest);
  my $str = "$maniid ".length($manifest).(defined($tag) ? " $tag" : '')."\n";
  if ($digestfile ne '-') {
    BSUtil::appendstr($digestfile, $str);
  } else {
    print $str;
  }
}

sub manifest_upload_tags {
  my ($manifest, $tags, $content_type) = @_;
  if (!@{$tags || []}) {
    manifest_upload($manifest, undef, $content_type);
    manifest_append($manifest, undef) if defined $digestfile;
    return;
  }
  for my $tag (BSUtil::unify(@$tags)) {
    manifest_upload($manifest, $tag, $content_type);
    manifest_append($manifest, $tag) if defined $digestfile;
  }
}

sub cosign_upload {
  my ($tag, $config, @layers) = @_;
  my $oci = 1;
  my ($config_ent, $config_blobid) = BSContar::make_blob_entry('config.json', $config);
  my $config_data = BSContar::create_config_data($config_ent, $oci);
  blob_upload($config_blobid, $config_ent);
  my @layer_data;
  while (@layers >= 2) {
    my ($payload_layer_data, $payload) = splice(@layers, 0, 2);
    my ($payload_ent, $payload_blobid) = BSContar::make_blob_entry($payload_layer_data->{'digest'}, $payload);
    die unless $payload_blobid eq $payload_layer_data->{'digest'};
    blob_upload($payload_blobid, $payload_ent);
    push @layer_data, $payload_layer_data;
  }
  my $mani = BSContar::create_dist_manifest_data($config_data, \@layer_data, $oci);
  my $mani_json = BSContar::create_dist_manifest($mani);
  return manifest_upload($mani_json, $tag, $mani->{'mediaType'});
}

sub get_all_tags {
  my @regtags;
  my $replyheaders;
  my $param = {
    'uri' => "$registryserver/v2/$repository/tags/list",
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
    'replyheaders' => \$replyheaders,
    'keepalive' => $keepalive,
  };
  while (1) {
    undef $replyheaders;
    my $r = BSRPC::rpc($param, \&JSON::XS::decode_json);
    push @regtags, @{$r->{'tags'} || []};
    last unless $replyheaders->{'link'};
    die unless $replyheaders->{'link'} =~ /^<(\/v2\/.*)>/;
    $param->{'uri'} = "$registryserver$1";
    $param->{'verbatim_uri'} = 1;
  }
  return BSUtil::unify(@regtags);
}

sub get_all_repositories {
  my @repos;
  my $replyheaders;
  my $param = {
    'uri' => "$registryserver/v2/_catalog",
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
    'replyheaders' => \$replyheaders,
  };
  while (1) {
    undef $replyheaders;
    my $r = BSRPC::rpc($param, \&JSON::XS::decode_json);
    push @repos, @{$r->{'repositories'}};
    last unless $replyheaders->{'link'};
    die("Bad link: $replyheaders->{'link'}\n") unless $replyheaders->{'link'} =~ /^<(\/v2\/\S+)>/;
    $param->{'uri'} = "$registryserver$1";
    $param->{'verbatim_uri'} = 1;
  }
  return BSUtil::unify(@repos);
}

sub get_manifest_for_tag {
  my ($tag) = @_;
  my $replyheaders;
  my $param = {
    'uri' => "$registryserver/v2/$repository/manifests/$tag",
    'headers' => [ "Accept: $BSContar::mt_docker_manifest, $BSContar::mt_docker_manifestlist, $BSContar::mt_oci_manifest, $BSContar::mt_oci_index" ],
    'authenticator' => $registry_authenticator,
    'replyheaders' => \$replyheaders,
    'timeout' => $registry_timeout,
    'keepalive' => $keepalive,
  };
  my $mani_json;
  eval { $mani_json = BSRPC::rpc($param); };
  if ($@) {
    return () if $@ =~ /^404/;	# tag does not exist
    die($@);
  }
  my $mani = JSON::XS::decode_json($mani_json);
  my $maniid = $replyheaders->{'docker-content-digest'};
  die("$tag: no docker-content-digest\n") unless $maniid;
  return ($mani, $maniid, $mani_json);
}

sub delete_manifest {
  my ($maniid) = @_;
  die("not a manifest digest: $maniid\n") unless $maniid =~ /^sha256:[0-9a-f]{64}$/;
  my ($mani, $maniid2, $mani_json) = get_manifest_for_tag($maniid);
  return unless $maniid2;
  die("manifest digest mismatch\n") if $maniid2 ne $maniid;
  print "deleting manifest $maniid\n";
  my $param = {
    'uri' => "$registryserver/v2/$repository/manifests/$maniid",
    'request' => 'DELETE',
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
  };
  BSRPC::rpc($param, undef);
}

my $cannot_directly_delete_tags;

sub delete_tag {
  my ($tag) = @_;

  my ($mani, $maniid, $mani_json) = get_manifest_for_tag($tag);
  return unless $maniid;
  print "deleting tag $tag [$maniid]\n";

  if (!$cannot_directly_delete_tags) {
    my $param = {
      'uri' => "$registryserver/v2/$repository/manifests/$tag",
      'request' => 'DELETE',
      'authenticator' => $registry_authenticator,
      'timeout' => $registry_timeout,
    };
    eval { BSRPC::rpc($param, undef); };
    return unless $@;
    die($@) unless $@ =~ /^40[05]/;
    $cannot_directly_delete_tags = 1;
  }

  # now mangle and upload so that we get a new unique image id for that tag
  my $mani_json_mangled = "\n\n".JSON::XS->new->utf8->canonical->encode($mani)."\n\n";
  my $newmaniid = manifest_upload($mani_json_mangled, $tag, $mani->{'mediaType'}, 1);

  # then delete the new unique image id, thus deleting the tag as well
  my $param = {
    'uri' => "$registryserver/v2/$repository/manifests/$newmaniid",
    'request' => 'DELETE',
    'authenticator' => $registry_authenticator,
    'timeout' => $registry_timeout,
  };
  eval { BSRPC::rpc($param, undef); };
  my $err = $@;

  if ($err) {
    # delete failed, switch tag back to the old image
    eval { manifest_upload($mani_json, $tag, $mani->{'mediaType'}, 1) if $newmaniid ne $maniid; };
    die($err);
  }
}

sub list_tag {
  my ($tag) = @_;

  if ($no_cosign_info && $tag =~ /^[a-z0-9]+-[a-f0-9]+\.(?:sig|att)$/) {
    printf "%-20s -\n", $tag;
    return;
  }
  my ($mani, $maniid) = get_manifest_for_tag($tag);
  my $extra = '';
  if ($tag =~ /^[a-z0-9]+-[a-f0-9]+\.sig$/ && $mani && $mani->{'mediaType'} && $mani->{'mediaType'} eq $BSContar::mt_oci_manifest) {
    if (@{$mani->{'layers'} || []} == 1 && $mani->{'layers'}->[0]->{'mediaType'} eq 'application/vnd.dev.cosign.simplesigning.v1+json') {
      my $annotations = $mani->{'layers'}->[0]->{'annotations'} || {};
      my $cookie = $annotations->{$cosign_cookie_name};
      $extra = " cosigncookie=$cookie" if $cookie;
    }
  }
  if ($tag =~ /^[a-z0-9]+-[a-f0-9]+\.att$/ && $mani && $mani->{'mediaType'} && $mani->{'mediaType'} eq $BSContar::mt_oci_manifest) {
    if (@{$mani->{'layers'} || []} >= 1 && $mani->{'layers'}->[0]->{'mediaType'} eq 'application/vnd.dsse.envelope.v1+json') {
      my $annotations = $mani->{'layers'}->[0]->{'annotations'} || {};
      my $cookie = $annotations->{$cosign_cookie_name};
      $extra = " cosigncookie=$cookie" if $cookie;
    }
  }
  if (!$mani) {
    printf "%-20s -\n", $tag;
  } elsif (!$mani->{'mediaType'}) {
    printf "%-20s %s %s\n", $tag, $maniid, 'v1image';
  } elsif ($mani->{'mediaType'} eq $BSContar::mt_docker_manifestlist) {
    printf "%-20s %s %s\n", $tag, $maniid, 'list';
  } elsif ($mani->{'mediaType'} eq $BSContar::mt_docker_manifest) {
    printf "%-20s %s %s%s\n", $tag, $maniid, 'image', $extra;
  } elsif ($mani->{'mediaType'} eq $BSContar::mt_oci_manifest) {
    printf "%-20s %s %s%s\n", $tag, $maniid, 'ociimage', $extra;
  } elsif ($mani->{'mediaType'} eq $BSContar::mt_oci_index) {
    printf "%-20s %s %s\n", $tag, $maniid, 'ocilist';
  } else {
    printf "%-20s %s %s\n", $tag, $maniid, 'unknown';
  }
}

sub tags_from_digestfile {
  my ($add_cosign_tags) = @_;
  return () unless $digestfile;
  my @ret;
  local *DIG;
  open(DIG, '<', $digestfile) || die("$digestfile: $!\n");
  while (<DIG>) {
    chomp;
    next if /^#/ || /^\s*$/;
    push @ret, "$1-$2.sig", "$1-$2.att" if $add_cosign_tags && /^([a-z0-9]+):([a-f0-9]+) (\d+)/;
    next if /^([a-z0-9]+):([a-f0-9]+) (\d+)\s*$/;	# ignore anonymous images
    die("bad line in digest file\n") unless /^([a-z0-9]+):([a-f0-9]+) (\d+) (.+?)\s*$/;
    push @ret, $4;
  }
  close(DIG);
  return @ret;
}

sub construct_container_tar {
  my ($containerinfo) = @_;

  die("Must specify a blobdir for containerinfos\n") unless $blobdir;
  my $manifest = $containerinfo->{'tar_manifest'};
  my $mtime = $containerinfo->{'tar_mtime'};
  my $blobids = $containerinfo->{'tar_blobids'};
  die("containerinfo is incomplete\n") unless $mtime && $manifest && $blobids;
  my @tar;
  for my $blobid (@$blobids) {
    my $fd;
    open($fd, '<', "$blobdir/_blob.$blobid") || die("$blobdir/_blob.$blobid: $!\n");
    push @tar, {'name' => $blobid, 'file' => $fd, 'mtime' => $mtime, 'offset' => 0, 'size' => (-s $fd)};
  }
  push @tar, {'name' => 'manifest.json', 'data' => $manifest, 'mtime' => $mtime, 'size' => length($manifest)};
  return \@tar;
}

sub open_tarfile {
  my ($tarfile) = @_;

  my ($tar, $tarfd, $containerinfo);
  if ($tarfile =~ /\.containerinfo$/) {
    my $containerinfo_json = readstr($tarfile);
    $containerinfo = JSON::XS::decode_json($containerinfo_json);
    $tar = construct_container_tar($containerinfo);
  } elsif ($tarfile =~ /\.helminfo$/) {
    my $chart = $tarfile;
    $chart =~ s/\.helminfo$/.tgz/;
    die("$chart: $!\n") unless -e $chart;
    my $helminfo_json = readstr($tarfile);
    my $helminfo = JSON::XS::decode_json($helminfo_json);
    ($tar) = BSContar::container_from_helm($chart, $helminfo->{'config_json'}, $helminfo->{'tags'});
  } else {
    open($tarfd, '<', $tarfile) || die("$tarfile: $!\n");
    $tar = BSTar::list($tarfd);
    $_->{'file'} = $tarfd for @$tar;
  }
  return ($tar, $tarfd, $containerinfo);
}

sub die_with_usage {
  die <<'END';
usage: bs_regpush [options] <registryserver> repository tar [tar...]
       bs_regpush [options] -l <registryserver> [repository [tag]]
       bs_regpush [options] -D <registryserver> repository
       bs_regpush [options] -X <registryserver> repository

MODES:
                - upload mode
  -l            - list mode
  -D            - delete mode
  -X            - delete except mode

OPTIONS:
  --dest-creds  - credentials in form <user>:<password> or "-" to read from STDIN
  -T            - use image tags
  -m            - push multiarch image
  -t            - tag (can be given multiple times)
  -F            - digestfile, output in upload mode, otherwise input

END
}

$| = 1;

while (@ARGV) {
  if ($ARGV[0] eq '--dest-creds') {
    $dest_creds = BSBearer::get_credentials($ARGV[1]);
    splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '-T') {
    $use_image_tags = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-m') {
    $multiarch = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-t') {
    push @tags, $ARGV[1];
    splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '-F') {
    $digestfile = $ARGV[1];
    splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '--write-info') {
    (undef, $writeinfofile) = splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '-D') {
    $delete_mode = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-l') {
    $list_mode = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '--no-cosign-info') {
    $no_cosign_info = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-X') {
    $delete_except_mode = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-B') {
    $blobdir = $ARGV[1];
    splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '--oci') {
    $oci = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '--cosign') {
    $cosign = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '--cosigncookie') {
    (undef, $cosigncookie) = splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '--slsaprovenance') {
    $slsaprovenance = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '--sbom') {
    $sbom = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '--rekor') {
    (undef, $rekorserver) = splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '-G') {
    (undef, $gun) = splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '-p') {
    (undef, $pubkeyfile) = splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '--dest-creds') {
    $dest_creds = BSBearer::get_credentials($ARGV[1]);
    splice(@ARGV, 0, 2);
  } elsif ($ARGV[0] eq '-P' || $ARGV[0] eq '--project' || $ARGV[0] eq '-u' || $ARGV[0] eq '--signtype' ||  $ARGV[0] eq '-h') {
    my @signopts = splice(@ARGV, 0, 2);
    push @signcmd, @signopts unless $signopts[0] eq '-h';
  } else {
    last;
  }
}

$registry_authenticator = BSBearer::generate_authenticator($dest_creds, 'verbose' => (-c STDOUT ? 1 : 0));

if ($list_mode) {
  ($registryserver, $repository) = @ARGV;
  if (@ARGV == 1) {
    for my $repo (sort(get_all_repositories())) {
      print "$repo\n";
    }
  } elsif (@ARGV == 2) {
    $keepalive = {};
    my %tags = map {$_ => 1} @tags;
    $tags{$_} = 1 for tags_from_digestfile();
    %tags = map {$_ => 1} get_all_tags() unless %tags;
    list_tag($_) for sort keys %tags;
  } elsif (@ARGV == 3) {
    my ($mani, $maniid, $mani_json) = get_manifest_for_tag($ARGV[2]);
    print "$mani_json\n" if $mani_json;
  } elsif (@ARGV == 4) {
    if ($ARGV[3] eq 'config' || $ARGV[3] eq 'config.json') {
      my ($mani) = get_manifest_for_tag($ARGV[2]);
      my $config = blob_fetch($mani->{'config'}->{'digest'});
      $config = JSON::XS->new->utf8->canonical->pretty->encode(JSON::XS::decode_json($config)) if $ARGV[3] eq 'config';
      print $config;
    } else {
      blob_download($ARGV[2], $ARGV[3]);
    }
  } else {
    die_with_usage();
  }
  exit(0);
}

die_with_usage() unless @ARGV >= 2;
($registryserver, $repository, @tarfiles) = @ARGV;

if ($delete_mode || $delete_except_mode) {
  die("cannot do both delete and delete-except\n") if $delete_mode && $delete_except_mode;
  my %tags;
  $tags{$_} = 1 for @tags;
  $tags{$_} = 1 for tags_from_digestfile($delete_except_mode ? 1 : 0);

  if ($delete_mode) {
    for my $tag (sort keys %tags) {
      if ($tag =~ /^sha256:[0-9a-f]{64}$/) {
        delete_manifest($tag);
      } else {
        delete_tag($tag);
      }
    }
  } elsif ($delete_except_mode) {
    for my $tag (grep {!$tags{$_}} get_all_tags()) {
      delete_tag($tag);
    }
  }
  exit;
}

if ($cosign) {
  require BSConfiguration;
  require BSConSign;
  require BSPGP;
  require BSX509 if $rekorserver;
  require BSRekor if $rekorserver;
  die("need a pubkey for cosign signature creation\n") unless $pubkeyfile;
  die("need a gun for cosign signature creation\n") unless $gun;
  die("sign program is not configured!\n") unless $BSConfig::sign;
  unshift @signcmd, $BSConfig::sign;
}


die("No tar file to upload?\n") if !@tarfiles;
die("more than one tar file specified\n") if @tarfiles > 1 && !$multiarch;

if ($use_image_tags && @tarfiles > 1) {
  # make sure all tar files contain the same tags
  my $imagetags;
  for my $tarfile (@tarfiles) {
    my ($tar, $tarfd) = open_tarfile($tarfile);
    my %tar = map {$_->{'name'} => $_} @$tar;
    my ($manifest_ent, $manifest) = BSContar::get_manifest(\%tar);
    my @imagetags = @{$manifest->{'RepoTags'} || []};
    s/.*:// for @imagetags;
    my $it = join(', ', sort(BSUtil::unify(@imagetags)));
    die("multiarch images contain different tags: $imagetags -- $it\n") if defined($imagetags) && $imagetags ne $it;
    if (!defined($imagetags)) {
      $imagetags = $it;
      push @tags, @imagetags;
    }
    close $tarfd if $tarfd;
  }
  $use_image_tags = 0;
}

# use oci types if we have a helm chart
$oci = 1 if grep {/\.helminfo$/} @tarfiles;

my %digests_to_sign;
my @multimanifests;
my %multiplatforms;
my @imginfos;
for my $tarfile (@tarfiles) {
  my ($tar, $tarfd, $containerinfo) = open_tarfile($tarfile);
  my %tar = map {$_->{'name'} => $_} @$tar;

  my $provenance;
  my $spdx_json;
  my $cyclonedx_json;
  if ($slsaprovenance) {
    my $provenancefile = $tarfile;
    if ($provenancefile =~ s/\.[^\.]*$/.slsa_provenance.json/) {
      $provenance = readstr($provenancefile) if -s $provenancefile;
    }
  }
  if ($sbom) {
    my $spdx_file = $tarfile;
    if ($spdx_file =~ s/\.[^\.]*$/.spdx.json/) {
      $spdx_json = readstr($spdx_file) if -s $spdx_file;
    }
    my $cyclonedx_file = $tarfile;
    if ($cyclonedx_file =~ s/\.[^\.]*$/.cdx.json/) {
      $cyclonedx_json = readstr($cyclonedx_file) if -s $cyclonedx_file;
    }
  }

  my ($manifest_ent, $manifest) = BSContar::get_manifest(\%tar);
  #print Dumper($manifest);

  if ($use_image_tags) {
    my @imagetags = @{$manifest->{'RepoTags'} || []};
    s/.*:// for @imagetags;
    push @tags, @imagetags if $use_image_tags;
  }

  my ($config_ent, $config) = BSContar::get_config(\%tar, $manifest);
  #print Dumper($config);

  my @layers = @{$manifest->{'Layers'} || []};
  die("container has no layers\n") unless @layers;
  my $config_layers;
  if ($config->{'rootfs'} && $config->{'rootfs'}->{'diff_ids'}) {
    $config_layers = $config->{'rootfs'}->{'diff_ids'};
    die("layer number mismatch\n") if @layers != @{$config_layers || []};
  }

  my $goarch = $config->{'architecture'} || 'any';
  my $goos = $config->{'os'} || 'any';
  my $govariant;
  $govariant = $containerinfo->{'govariant'} if $containerinfo && $containerinfo->{'govariant'};
  $govariant = $config->{'variant'} if $config->{'variant'};
  if ($multiarch) {
    # see if a already have this arch/os combination
    my $platformstr = "architecture:$goarch os:$goos";
    $platformstr .= " variant:$govariant" if $govariant;
    if ($multiplatforms{$platformstr}) {
      print "ignoring $tarfile, already have $platformstr\n";
      close $tarfd if $tarfd;
      next;
    }
    $multiplatforms{$platformstr} = 1;
  }


  # process config
  my $config_data = BSContar::create_config_data($config_ent, $oci);
  my $config_blobid = $config_data->{'digest'};

  # upload to server
  blob_upload($config_blobid, $config_ent);

  # process layers (compress if necessary)
  my %layer_datas;
  my @layer_data;
  my @layer_comp = @{($containerinfo || {})->{'layer_compression'} || []};
  for my $layer_file (@layers) {
    my $lcomp = shift @layer_comp;
    if ($layer_datas{$layer_file}) {
      # already did that file, just reuse old layer data
      push @layer_data, $layer_datas{$layer_file};
      next;
    }
    my $layer_ent = $tar{$layer_file};
    die("File $layer_file not included in tar\n") unless $layer_ent;
    # normalize layer
    ($layer_ent, $lcomp) = BSContar::normalize_layer($layer_ent, $oci, undef, undef, $lcomp);
    # create layer data
    my $layer_data = BSContar::create_layer_data($layer_ent, $oci, $lcomp);
    push @layer_data, $layer_data;
    $layer_datas{$layer_file} = $layer_data;

    # upload to server
    blob_upload($layer_data->{'digest'}, $layer_ent);
  }
  close $tarfd if $tarfd;

  my $mani = BSContar::create_dist_manifest_data($config_data, \@layer_data, $oci);
  my $mediaType = $mani->{'mediaType'};
  my $mani_json = BSContar::create_dist_manifest($mani);
  my $mani_id = BSContar::blobid($mani_json);
  $digests_to_sign{$mani_id} = [ $provenance, $spdx_json, $cyclonedx_json ];

  if ($multiarch) {
    manifest_upload_tags($mani_json, undef, $mediaType);	# upload anonymous image
    my $multimani = {
      'mediaType' => $mediaType,
      'size' => length($mani_json),
      'digest' => $mani_id,
      'platform' => {'architecture' => $goarch, 'os' => $goos},
    };
    $multimani->{'platform'}->{'variant'} = $govariant if $govariant;
    push @multimanifests, $multimani;
  } else {
    manifest_upload_tags($mani_json, \@tags, $mediaType);
  }
  my $imginfo = {
    'file' => $tarfile,
    'imageid' => $config_blobid,
    'goarch' => $goarch,
    'goos' => $goos,
    'distmanifest' => $mani_id,
  };
  $imginfo->{'govariant'} = $govariant if $govariant;
  my @diff_ids = @{$config_layers || []};
  for (@layer_data) {
    my $l = { 'blobid' => $_->{'digest'}, 'blobsize' => $_->{'size'} };
    $l->{'diffid'} = shift @diff_ids if @diff_ids;
    push @{$imginfo->{'layers'}}, $l;
  }
  push @imginfos, $imginfo;
}

my $info = {
  'images' => \@imginfos,
  'tags' => \@tags,
  'distmanifesttype' => 'image',
  'distmanifest' => $imginfos[0]->{'distmanifest'},
};

if ($multiarch) {
  my $mani = BSContar::create_dist_manifest_list_data(\@multimanifests, $oci);
  my $mediaType = $mani->{'mediaType'};
  my $mani_json = BSContar::create_dist_manifest_list($mani);
  my $mani_id = BSContar::blobid($mani_json);
  $digests_to_sign{$mani_id} = [];
  manifest_upload_tags($mani_json, \@tags, $mediaType);
  $info->{'distmanifesttype'} = 'list';
  $info->{'distmanifest'} = $mani_id;
}

if ($writeinfofile) {
  my $info_json = JSON::XS->new->utf8->canonical->encode($info);
  writestr($writeinfofile, undef, $info_json);
}

if ($cosign && %digests_to_sign) {
  my $creator = 'OBS';
  my $gpgpubkey = readstr($pubkeyfile);
  $cosigncookie ||= BSConSign::createcosigncookie($gpgpubkey, $gun, $creator);
  # upload signatures
  for my $digest (sort keys %digests_to_sign) {
    my $sig_tag = "$digest.sig";
    $sig_tag =~ s/:/-/;
    my ($sig_mani, $sig_maniid, $sig_mani_json) = get_manifest_for_tag($sig_tag);
    if ($sig_mani && $sig_mani->{'mediaType'} && $sig_mani->{'mediaType'} eq $BSContar::mt_oci_manifest && @{$sig_mani->{'layers'} || []} == 1 && $BSConSign::mt_cosign && $sig_mani->{'layers'}->[0]->{'mediaType'} eq $BSConSign::mt_cosign) {
      my $annotations = $sig_mani->{'layers'}->[0]->{'annotations'} || {};
      next if ($annotations->{$cosign_cookie_name} || '') eq $cosigncookie;
    }
    print "creating cosign signature for $gun $digest\n";
    my $signfunc =  sub { BSUtil::xsystem($_[0], @signcmd, '-O', '-h', 'sha256') };
    my $annotations = { $cosign_cookie_name => $cosigncookie };
    my ($config, $payload_layer, $payload, $sig) = BSConSign::createcosign($signfunc, $digest, $gun, $creator, undef, $annotations);
    cosign_upload($sig_tag, $config, $payload_layer, $payload);
    if ($rekorserver) {
      print "uploading cosign signature to $rekorserver\n";
      my $sslpubkey = BSX509::keydata2pubkey(BSPGP::pk2keydata(BSPGP::unarmor($gpgpubkey)));
      $sslpubkey = BSASN1::der2pem($sslpubkey, 'PUBLIC KEY');
      BSRekor::upload_hashedrekord($rekorserver, $payload_layer->{'digest'}, $sslpubkey, $sig);
    }
  }
  # upload attestations
  for my $digest (sort keys %digests_to_sign) {
    my $provenance = $digests_to_sign{$digest}->[0];
    my $spdx_json = $digests_to_sign{$digest}->[1];
    my $cyclonedx_json = $digests_to_sign{$digest}->[2];
    next unless $provenance || $spdx_json || $cyclonedx_json;
    my $att_tag = "$digest.att";
    $att_tag =~ s/:/-/;
    my ($att_mani, $att_maniid, $att_mani_json) = get_manifest_for_tag($att_tag);
    my $numlayers = ($provenance ? 1 : 0) + ($spdx_json ? 1 : 0) + ($cyclonedx_json ? 1 : 0);
    if ($att_mani && $att_mani->{'mediaType'} && $att_mani->{'mediaType'} eq $BSContar::mt_oci_manifest && @{$att_mani->{'layers'} || []} == $numlayers && $BSConSign::mt_dsse) {
      next unless grep {$_->{'mediaType'} eq $BSConSign::mt_dsse && (($_->{'annotations'} || {})->{$cosign_cookie_name} || '') eq $cosigncookie} @{$att_mani->{'layers'}};
    }
    print "creating cosign attestation for $gun $digest\n";
    my $signfunc =  sub { BSUtil::xsystem($_[0], @signcmd, '-O', '-h', 'sha256') };
    my $annotations = { $cosign_cookie_name => $cosigncookie };
    my @attestations;
    push @attestations, BSConSign::fixup_intoto_attestation($provenance, $signfunc, $digest, $gun) if $provenance;
    push @attestations, BSConSign::fixup_intoto_attestation($spdx_json, $signfunc, $digest, $gun) if $spdx_json;
    push @attestations, BSConSign::fixup_intoto_attestation($cyclonedx_json, $signfunc, $digest, $gun) if $cyclonedx_json;
    my ($config, @attestation_layers) = BSConSign::createcosign_attestation($digest, \@attestations, $annotations);
    cosign_upload($att_tag, $config, @attestation_layers);
    if ($rekorserver) {
      print "uploading cosign attestations to $rekorserver\n";
      my $sslpubkey = BSX509::keydata2pubkey(BSPGP::pk2keydata(BSPGP::unarmor($gpgpubkey)));
      $sslpubkey = BSASN1::der2pem($sslpubkey, 'PUBLIC KEY');
      for my $attestation (@attestations) {
        BSRekor::upload_intoto($rekorserver, $attestation, $sslpubkey);
      }
    }
  }
}

