Skip to content

Commit

Permalink
Merge pull request #12110 from mlschroe/master
Browse files Browse the repository at this point in the history
[backend] implement cosign signature creation for external registries
  • Loading branch information
mlschroe committed Jan 19, 2022
2 parents c291a2e + ff76b74 commit 64cb80b
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 30 deletions.
10 changes: 6 additions & 4 deletions src/backend/BSConSign.pm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ use Digest::SHA ();
use MIME::Base64 ();
use IO::Compress::RawDeflate;

our $mt_cosign = 'application/vnd.dev.cosign.simplesigning.v1+json';

sub canonical_json {
return JSON::XS->new->utf8->canonical->encode($_[0]);
}
Expand Down Expand Up @@ -83,7 +85,7 @@ sub sig2openshift {
}

sub createcosign {
my ($signfunc, $digest, $reference, $creator, $timestamp) = @_;
my ($signfunc, $digest, $reference, $creator, $timestamp, $annotations) = @_;
my $payload = createpayload('cosign container image signature', $digest, $reference, $creator, $timestamp);
my $payload_digest = 'sha256:'.Digest::SHA::sha256_hex($payload);
# signfunc must return the openssl rsa signature
Expand All @@ -98,12 +100,12 @@ sub createcosign {
};
my $config_json = canonical_json($config);
my $payload_layer = {
'annotations' => { 'dev.cosignproject.cosign/signature' => $sig },
'annotations' => { 'dev.cosignproject.cosign/signature' => $sig, %{$annotations || {}} },
'digest' => $payload_digest,
'mediaType' => 'application/vnd.dev.cosign.simplesigning.v1+json',
'mediaType' => $mt_cosign,
'size' => length($payload),
};
return ($config_json, $payload_layer, $payload);
return ($config_json, $payload_layer, $payload, $sig);
}

1;
21 changes: 18 additions & 3 deletions src/backend/BSPublisher/Container.pm
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ sub upload_all_containers {
for my $joinp (sort keys %todo) {
my @tags = @{$todo{$joinp}};
my @containerinfos = map {$containers->{$_}} @{$todo_p{$joinp}};
my ($digest, @refs) = upload_to_registry($registry, \@containerinfos, $repository, \@tags);
my ($digest, @refs) = upload_to_registry($registry, \@containerinfos, $repository, \@tags, $projid, $signargs, $pubkey);
add_notary_upload($notary_uploads, $registry, $repository, $digest, \@tags);
$containerdigests .= $digest;
push @{$allrefs{$_}}, @refs for @{$todo_p{$joinp}};
Expand Down Expand Up @@ -347,15 +347,14 @@ sub reconstruct_container {
containerinfos - array of containers to upload (more than one for multiarch)
repository - registry repository name
tags - array of tags to upload to
notary_uploads - hash to store notary information
Returns:
containerdigests + public references to uploaded containers
=cut

sub upload_to_registry {
my ($registry, $containerinfos, $repository, $tags) = @_;
my ($registry, $containerinfos, $repository, $tags, $projid, $signargs, $pubkey) = @_;

return unless @{$containerinfos || []} && @{$tags || []};

Expand Down Expand Up @@ -401,6 +400,22 @@ sub upload_to_registry {
my @opts = map {('-t', $_)} @$tags;
push @opts, '-m' if @uploadfiles > 1; # create multi arch container
push @opts, '-B', $blobdir if $blobdir;
my $cosign = $registry->{'cosign'};
$cosign = $cosign->($repository, $projid) if $cosign && ref($cosign) eq 'CODE';
if (defined($pubkey) && $cosign) {
my $gun = $registry->{'notary_gunprefix'} || $registry->{'server'};
$gun =~ s/^https?:\/\///;
$gun .= "/$repository";
my @signargs;
push @signargs, '--project', $projid if $BSConfig::sign_project;
push @signargs, @{$signargs || []};
my $pubkeyfile = "$uploaddir/publisher.$$.pubkey";
push @tempfiles, $pubkeyfile;
mkdir_p($uploaddir);
unlink($pubkeyfile);
writestr($pubkeyfile, undef, $pubkey);
push @opts, '--cosign', '-p', $pubkeyfile, '-G', $gun, @signargs;
}
my @cmd = ("$INC[0]/bs_regpush", '--dest-creds', '-', @opts, '-F', $containerdigestfile, $registryserver, $repository, @uploadfiles);
print "Uploading to registry: @cmd\n";
my $result = BSPublisher::Util::qsystem('echo', "$registry->{user}:$registry->{password}\n", 'stdout', '', @cmd);
Expand Down
49 changes: 31 additions & 18 deletions src/backend/BSPublisher/Registry.pm
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@

package BSPublisher::Registry;

use Digest::SHA;
use Digest::SHA ();
use JSON::XS ();
use MIME::Base64 ();

use BSConfiguration;
use BSUtil;
Expand All @@ -32,7 +33,10 @@ use BSPublisher::Blobstore;
use BSContar;
use BSRPC;
use BSTUF;
use BSPGP;
use BSX509;
use BSConSign;
use BSRekor;

my $registrydir = "$BSConfig::bsdir/registry";
my $uploaddir = "$BSConfig::bsdir/upload";
Expand Down Expand Up @@ -141,14 +145,14 @@ sub push_blob {
return $blobid;
}

sub push_blob_string {
my ($repodir, $blobdata) = @_;
my $blob_id = 'sha256:'.Digest::SHA::sha256_hex($blobdata);
sub push_blob_content {
my ($repodir, $content) = @_;
my $blob_id = 'sha256:'.Digest::SHA::sha256_hex($content);
my $dir = "$repodir/:blobs";
return $blob_id if -e "$dir/$blob_id";
mkdir_p($dir) unless -d $dir;
unlink("$dir/.$blob_id.$$");
writestr("$dir/.$blob_id.$$", "$dir/$blob_id", $blobdata);
writestr("$dir/.$blob_id.$$", "$dir/$blob_id", $content);
return $blob_id;
}

Expand Down Expand Up @@ -215,7 +219,7 @@ sub gen_timestampkey {
$pubkey = BSPGP::unarmor($pubkey);
$pubkey = BSPGP::pk2keydata($pubkey);
die unless $pubkey;
$pubkey = BSTUF::keydata2asn1($pubkey);
$pubkey = BSX509::keydata2pubkey($pubkey);
$pubkey = MIME::Base64::encode_base64($pubkey, '');
return ($privkey, $pubkey);
}
Expand All @@ -240,7 +244,7 @@ sub update_tuf {
die("need an rsa pubkey for container signing\n") unless ($pubkey_data->{'algo'} || '') eq 'rsa';
my $pubkey_times = BSPGP::pk2times($gpgpubkey) || {};
# generate pub key and cert from pgp key data
my $pub_bin = BSTUF::keydata2asn1($pubkey_data);
my $pub_bin = BSX509::keydata2pubkey($pubkey_data);

my $root_expire = $pubkey_times->{'key_expire'} + $root_extra_expire;
my $tbscert = BSTUF::mktbscert($gun, $pubkey_times->{'selfsig_create'}, $root_expire, $pub_bin);
Expand Down Expand Up @@ -432,7 +436,7 @@ sub update_sigs {
}

sub update_cosign {
my ($prp, $repo, $gun, $imagedigests, $pubkey, $signargs, $oci, $knownmanifests, $knownblobs) = @_;
my ($prp, $repo, $gun, $digests_to_cosign, $pubkey, $signargs, $rekorserver, $knownmanifests, $knownblobs) = @_;

my $creator = 'OBS';
my ($projid, $repoid) = split('/', $prp, 2);
Expand All @@ -443,27 +447,28 @@ sub update_cosign {
my $signfunc = sub { BSUtil::xsystem($_[0], @signcmd, '-O', '-h', 'sha256') };
my $repodir = "$registrydir/$repo";
my $oldsigs = BSUtil::retrieve("$repodir/:cosign", 1) || {};
return if !%$oldsigs && !%$imagedigests;
return if !%$oldsigs && !%$digests_to_cosign;
my $gpgpubkey = BSPGP::unarmor($pubkey);
my $pubkey_fp = BSPGP::pk2fingerprint($gpgpubkey);
if (($oldsigs->{'pubkey'} || '') ne $pubkey_fp || ($oldsigs->{'gun'} || '') ne $gun || ($oldsigs->{'creator'} || '') ne ($creator || '')) {
$oldsigs = {}; # fingerprint/gun/creator mismatch, do not use old signatures
}
my $sigs = { 'pubkey' => $pubkey_fp, 'gun' => $gun, 'creator' => $creator, 'digests' => {} };
for my $digest (sort keys %$imagedigests) {
for my $digest (sort keys %$digests_to_cosign) {
my $oci = $digests_to_cosign->{$digest};
my $old = ($oldsigs->{'digests'} || {})->{$digest};
if ($old) {
$sigs->{'digests'}->{$digest} = $old;
next;
}
print "creating cosign signature for $gun $digest\n";
my ($config, $payload_layer, $payload) = BSConSign::createcosign($signfunc, $digest, $gun, $creator);
my $config_blobid = push_blob_string($repodir, $config);
my ($config, $payload_layer, $payload, $sig) = BSConSign::createcosign($signfunc, $digest, $gun, $creator);
my $config_blobid = push_blob_content($repodir, $config);
$knownblobs->{$config_blobid} = 1;
my $payload_blobid = push_blob_string($repodir, $payload);
my $payload_blobid = push_blob_content($repodir, $payload);
$knownblobs->{$payload_blobid} = 1;
die unless $payload_blobid eq $payload_layer->{'digest'};
my $config_data = {
my $config_data = {
'mediaType' => $oci ? $BSContar::mt_oci_config : $BSContar::mt_docker_config,
'size' => length($config),
'digest' => $config_blobid,
Expand All @@ -479,6 +484,11 @@ sub update_cosign {
my $mani_id = push_manifest($repodir, $mani_json);
$knownmanifests->{$mani_id} = 1;
$sigs->{'digests'}->{$digest} = $mani_id;
if ($rekorserver) {
print "uploading cosign signature to $rekorserver\n";
my $sslpubkey = BSX509::keydata2pubkey(BSPGP::pk2keydata($gpgpubkey));
BSRekor::upload_hashedrekord($rekorserver, $payload_layer->{'digest'}, $sslpubkey, $sig);
}
}
if (BSUtil::identical($oldsigs, $sigs)) {
print "local cosign signatures: no change.\n";
Expand All @@ -492,7 +502,7 @@ sub update_cosign {
}

sub push_containers {
my ($prp, $repo, $gun, $multiarch, $tags, $pubkey, $signargs) = @_;
my ($prp, $repo, $gun, $multiarch, $tags, $pubkey, $signargs, $rekorserver) = @_;

my $containerdigests = '';

Expand All @@ -511,6 +521,7 @@ sub push_containers {
my %knownmanifests;
my %knowntags;
my %knownimagedigests;
my %digests_to_cosign;

my %info;

Expand Down Expand Up @@ -627,6 +638,7 @@ sub push_containers {
my $mani_json = BSContar::create_dist_manifest($mani);
my $mani_id = push_manifest($repodir, $mani_json);
$knownmanifests{$mani_id} = 1;
$digests_to_cosign{$mani_id} = $oci;

my $multimani = {
'mediaType' => $mediaType,
Expand Down Expand Up @@ -680,6 +692,7 @@ sub push_containers {
$mani_size = length($mani_json);
$knownmanifests{$mani_id} = 1;
$taginfo->{'distmanifesttype'} = 'list';
$digests_to_cosign{$mani_id} = $oci;
} else {
$mani_id = $multimanifests[0]->{'digest'};
$mani_size = $multimanifests[0]->{'size'};
Expand All @@ -692,9 +705,9 @@ sub push_containers {
$info{$tag} = $taginfo;
}

# write signatures file
if ($gun && %knownimagedigests) {
update_cosign($prp, $repo, $gun, \%knownimagedigests, $pubkey, $signargs, 0, \%knownmanifests, \%knownblobs);
# write signatures file (need to do this early as it adds manifests/blobs)
if ($gun && %digests_to_cosign) {
update_cosign($prp, $repo, $gun, \%digests_to_cosign, $pubkey, $signargs, $rekorserver, \%knownmanifests, \%knownblobs);
} elsif (-e "$repodir/:cosign") {
unlink("$repodir/:cosign");
}
Expand Down
138 changes: 138 additions & 0 deletions src/backend/BSRekor.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#
# Copyright (c) 2022 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
#
################################################################
#
# Sigstore rekor support
#

package BSRekor;

use strict;

use JSON::XS ();
use MIME::Base64 ();

use BSRPC ':https';
use BSConfiguration;

our $intoto_slsav1 = "https://slsa.dev/provenance/v0.1";
our $intoto_spdx = "https://spdx.dev/Document";
our $intoto_linkv1= "https://in-toto.io/Link/v1";
our $intoto_cosignv1 = "cosign.sigstore.dev/attestation/v1";

sub canonical_json {
my ($d) = @_;
return JSON::XS->new->utf8->canonical->encode($d);
}

sub mime_encode {
my ($d) = @_;
return MIME::Base64::encode_base64($d, '');
}

sub dsse_pae {
my ($type, $payload) = @_;
return sprintf("DSSEv1 %d %s %d ", length($type), $type, length($payload))."$payload";
}

sub upload_entry {
my ($server, $entry) = @_;
my $replyheaders;
my $param = {
'uri' => "$server/api/v1/log/entries",
'request' => 'POST',
'timeout' => 300,
'data' => canonical_json($entry),
'headers' => [ 'Content-Type: application/json' ],
'replyheaders' => \$replyheaders,
'ignorestatus' => 1,
};
my $r = BSRPC::rpc($param);
my $st = $replyheaders->{'status'};
if ($st !~ /^201|409/) {
my $msg = eval { (JSON::XS::decode_json($r) || {})->{'message'} };
die("rekor server: $st ($msg)\n") if $msg;
die("rekor server: $st\n");
}
# entry created or already exists
my $l = $replyheaders->{'location'};
die("rekor server did not return a location\n") unless $l;
$l =~ s/.*\///;
return $l;
# new: {"84f4192f5c38c9eb0973dae7bdd24e0ad6781d9e228b4ee60f411ea0e1050482":{"body":"...","integratedTime":1638195094,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":897532,"verification":{"signedEntryTimestamp":"MEQCIDd8LwH1lbeUfCjwRoX5J7fzZ5qIK4PwMsf+sHJHgCCTAiB1h1nD0OjeleiBph8UtlZMTwlpNLq3cSaZ0Oxc8Gom0A=="}}}
}

sub upload_rekord {
my ($server, $content, $pubkey, $signature, $sigformat) = @_;
my $sig = {
'format' => $sigformat,
'content' => mime_encode($signature),
'publicKey' => { 'content' => mime_encode($pubkey) },
};
my $spec = {
'data' => { 'content' => mime_encode($content) },
'signature' => $sig,
};
my $entry = {
'kind' => 'rekord',
'apiVersion' => '0.0.1',
'spec' => $spec,
};
return upload_entry($server, $entry);
}

sub upload_hashedrekord {
my ($server, $hash, $pubkey, $signature) = @_;
die("bad hash $hash\n") unless $hash =~ /^(.+?):([0-9a-f]+)$/;
my ($hashalgo, $hashvalue) = ($1, $2);
my $sig = {
'content' => mime_encode($signature),
'publicKey' => { 'content' => mime_encode($pubkey) },
};
my $spec = {
'data' => { 'hash' => { 'algorithm' => $hashalgo, 'value' => $hashvalue } },
'signature' => $sig,
};
my $entry = {
'kind' => 'hashedrekord',
'apiVersion' => '0.0.1',
'spec' => $spec,
};
return upload_entry($server, $entry);
}

sub upload_intoto {
my ($server, $pubkey, $payloadtype, $payload, $signature) = @_;
my $envelope = {
'payload' => mime_encode($payload),
'payloadType' => $payloadtype,
'signatures' => [ { 'sig' => mime_encode($signature) } ],
};
my $spec = {
'publicKey' => mime_encode($pubkey),
'content' => { 'envelope' => canonical_json($envelope) },
};
my $entry = {
'kind' => 'intoto',
'apiVersion' => '0.0.1',
'spec' => $spec,
};
return upload_entry($server, $entry);
}

1;
Loading

0 comments on commit 64cb80b

Please sign in to comment.