Skip to content
Permalink
1e051bb20f
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
executable file 937 lines (858 sloc) 30.5 KB
#!/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) = @_;
return $blobid if blob_exists($blobid, $upload_ent->{'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: $upload_ent->{'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_upload_content {
my ($blobid, $content) = @_;
return blob_upload($blobid, { 'data' => $content, 'size' => length($content) });
}
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 = 'sha256:'.Digest::SHA::sha256_hex($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 ||= 'sha256:'.Digest::SHA::sha256_hex($maniret);
return 1 if $maniretid eq $maniid;
}
return 0;
}
sub manifest_upload {
my ($manifest, $tag, $content_type, $quiet) = @_;
my $maniid = 'sha256:'.Digest::SHA::sha256_hex($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 = 'sha256:'.Digest::SHA::sha256_hex($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 $config_blobid = blob_upload_content('sha256:'.Digest::SHA::sha256_hex($config), $config);
my $config_data = {
'mediaType' => $BSContar::mt_oci_config,
'size' => length($config),
'digest' => $config_blobid,
};
my $mediaType = $BSContar::mt_oci_manifest;
my $mani = {
'schemaVersion' => 2,
'mediaType' => $mediaType,
'config' => $config_data,
'layers' => [],
};
while (@layers >= 2) {
my ($payload_layer, $payload) = splice(@layers, 0, 2);
my $payload_blobid = blob_upload_content('sha256:'.Digest::SHA::sha256_hex($payload), $payload);
die unless $payload_blobid eq $payload_layer->{'digest'};
push @{$mani->{'layers'}}, $payload_layer;
}
my $mani_json = BSContar::create_dist_manifest_list($mani);
return manifest_upload($mani_json, $tag, $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, $govariant);
if ($tarfile =~ /\.containerinfo$/) {
my $containerinfo_json = readstr($tarfile);
my $containerinfo = JSON::XS::decode_json($containerinfo_json);
$govariant = $containerinfo->{'govariant'};
$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, $govariant);
}
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, $govariant) = 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';
$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_blobid = BSContar::blobid_entry($config_ent);
# create layer data
my $config_data = {
'mediaType' => $config_ent->{'mimetype'} || ($oci ? $BSContar::mt_oci_config : $BSContar::mt_docker_config),
'size' => $config_ent->{'size'},
'digest' => $config_blobid,
};
# upload to server
blob_upload($config_blobid, $config_ent);
# process layers (compress if necessary)
my %layer_datas;
my @layer_data;
for my $layer_file (@layers) {
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;
# detect layer compression
my $comp = BSContar::detect_entry_compression($layer_ent);
die("unsupported compression $comp\n") if $comp && $comp ne 'gzip';
if (!$comp) {
print "compressing $layer_ent->{'name'}... ";
$layer_ent = BSContar::compress_entry($layer_ent);
print "done.\n";
}
my $blobid = BSContar::blobid_entry($layer_ent);
#print "$layer_file -> $blobid\n";
# create layer data
my $layer_data = {
'mediaType' => $layer_ent->{'mimetype'} || ($oci ? $BSContar::mt_oci_layer_gzip : $BSContar::mt_docker_layer_gzip),
'size' => $layer_ent->{'size'},
'digest' => $blobid,
};
push @layer_data, $layer_data;
$layer_datas{$layer_file} = $layer_data;
# upload to server
blob_upload($blobid, $layer_ent);
}
close $tarfd if $tarfd;
my $mediaType = $oci ? $BSContar::mt_oci_manifest : $BSContar::mt_docker_manifest;
my $mani = {
'schemaVersion' => 2,
'mediaType' => $mediaType,
'config' => $config_data,
'layers' => \@layer_data,
};
my $mani_json = BSContar::create_dist_manifest($mani);
my $mani_id = 'sha256:'.Digest::SHA::sha256_hex($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 $mediaType = $oci ? $BSContar::mt_oci_index : $BSContar::mt_docker_manifestlist;
my $mani = {
'schemaVersion' => 2,
'mediaType' => $mediaType,
'manifests' => \@multimanifests,
};
my $mani_json = BSContar::create_dist_manifest_list($mani);
my $mani_id = 'sha256:'.Digest::SHA::sha256_hex($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);
}
}
}
}