Skip to content

Commit

Permalink
[backend] add support for slsa provenance binary pinning
Browse files Browse the repository at this point in the history
We link the used binaries into $BSConfig::bsdir/slsa. An
sqlite database is used to save the reference information so
that no longer referenced binaries can be deleted.
  • Loading branch information
mlschroe committed Apr 26, 2022
1 parent 39a0b28 commit 3a5033d
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 1 deletion.
160 changes: 160 additions & 0 deletions src/backend/BSRepServer/SLSA.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Copyright (c) 2022 SUSE LLC
#
# 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
#
package BSRepServer::SLSA;

use strict;

use BSConfiguration;
use BSUtil;
use BSSQLite;
use Digest::SHA ();
use Data::Dumper;

use DBI qw(:sql_types);

my $reporoot = "$BSConfig::bsdir/build";
my $slsadir = "$BSConfig::bsdir/slsa";

sub sha256file {
my ($fn) = @_;
my $fd;
open($fd, '<', $fn) || die("$fn: $!\n");
my $ctx = Digest::SHA->new(256);
$ctx->addfile($fd);
close($fd);
return $ctx->hexdigest();
}

sub connectdb {
my ($prpa) = @_;
mkdir_p("$slsadir/$prpa");
my $h = BSSQLite::connectdb("$slsadir/$prpa/refs");
create_tables($h);
return $h;
}

sub create_tables {
my ($h) = @_;
BSSQLite::dbdo($h, <<'EOS');
CREATE TABLE IF NOT EXISTS refs(
prpa TEXT,
digest BLOB
)
EOS
BSSQLite::dbdo($h, 'CREATE INDEX IF NOT EXISTS refs_idx_prpa on refs(prpa)');
BSSQLite::dbdo($h, 'CREATE INDEX IF NOT EXISTS refs_idx_digest on refs(digest)');
}

sub read_gbininfo {
my ($prpa) = @_;
my $gdst = "$reporoot/$prpa";
my $gbininfo = BSUtil::retrieve("$gdst/:bininfo", 1) || {};
return $gbininfo unless -e "$gdst/:bininfo.merge";
my $gbininfo_m = BSUtil::retrieve("$gdst/:bininfo.merge", 1);
$gbininfo_m = undef if $gbininfo_m && $gbininfo_m->{'/outdated'};
if ($gbininfo_m) {
for (keys %$gbininfo_m) {
if ($gbininfo_m->{$_}) {
$gbininfo->{$_} = $gbininfo_m->{$_};
} else {
delete $gbininfo->{$_};
}
}
}
return $gbininfo;
}

sub link_binary {
my ($prpa, $gbininfo, $hint, $digest, $tmp) = @_;

my $binname1 = '';
my $binname2 = '';
$binname1 = $hint;
$binname1 =~ s/\.[^\.]+$//;
$binname2 = $1 if $hint =~ /(.*)-([^-]+)-([^-]+)\.([^-]+)\.rpm$/;
for my $packid (sort keys %$gbininfo) {
for my $k (sort keys %{$gbininfo->{$packid}}) {
my $ent = $gbininfo->{$packid}->{$k};
next unless $ent->{'name'} && ($ent->{'name'} eq $binname1 || $ent->{'name'} eq $binname2);
unlink($tmp);
next unless link("$reporoot/$prpa/$packid/$ent->{'filename'}", $tmp);
unlink("$tmp.prov");
link("$reporoot/$prpa/$packid/_slsa_provenance_stmt.json", "$tmp.prov");
return 1 if sha256file($tmp) eq $digest;
unlink($tmp);
unlink("$tmp.prov");
}
}
return 0;
}

sub link_binaries {
my ($prpa, $digests) = @_;

my $gbininfo;
for my $digest (sort keys %$digests) {
next if -e "$slsadir/$prpa/$digest";
mkdir_p("$slsadir/$prpa");
$gbininfo ||= read_gbininfo($prpa);
my $tmp = "$slsadir/$prpa/.incoming$$";
die("404 binary $digests->{$digest} digest $digest does not exist in $prpa\n") unless link_binary($prpa, $gbininfo, $digests->{$digest}, $digest, $tmp);
if (!link($tmp, "$slsadir/$prpa/$digest")) {
my $err = "link $slsadir/$prpa/.incoming$$ $slsadir/$prpa/$digest: $!";
unlink($tmp);
unlink("$tmp.prov");
die("$err\n") unless -e "$slsadir/$prpa/$digest";
} else {
link("$tmp.prov", "$slsadir/$prpa/$digest.prov");
unlink($tmp);
unlink("$tmp.prov");
}
}
}

sub add_references {
my ($prpa, $refprpa, $digests) = @_;

link_binaries($prpa, $digests);
my $h = connectdb($prpa);
BSSQLite::begin_work($h);
my $got = $h->selectcol_arrayref("SELECT digest FROM refs WHERE prpa = ?", undef, $refprpa) || die($h->errstr);
my %got = map {pack("H*", $_) => 1} @$got;
for my $digest (grep {!$got{$_}} sort keys %$digests) {
BSSQLite::dbdo_bind($h, 'INSERT INTO refs(prpa,digest) VALUES(?,?)', [ $refprpa ], [ unpack("H*", $digest), SQL_BLOB ]);
}
BSSQLite::commit($h);
}

sub set_references {
my ($prpa, $refprpa, $digests) = @_;

link_binaries($prpa, $digests) if $digests;
my $h = connectdb($prpa);
BSSQLite::begin_work($h);
my $got = $h->selectcol_arrayref("SELECT digest FROM refs WHERE prpa = ?", undef, $refprpa) || die($h->errstr);
my %got = map {pack("H*", $_) => 1} @$got;
for my $digest (grep {!$got{$_}} sort keys %$digests) {
BSSQLite::dbdo_bind($h, 'INSERT INTO refs(prpa,digest) VALUES(?,?)', [ $refprpa ], [ unpack("H*", $digest), SQL_BLOB ]);
}
delete $got{$_} for keys %$digests;
for my $digest (sort keys %$got) {
BSSQLite::dbdo_bind($h, 'DELETE FROM refs WHERE prpa = ? AND digest = ?', [ $refprpa ], [ unpack("H*", $digest), SQL_BLOB ]);
}
BSSQLite::commit($h);
}

1;
13 changes: 13 additions & 0 deletions src/backend/bs_repserver
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ use BSRepServer::Remote;
use BSRepServer::Registry;
use BSRepServer::YMP;
use BSRepServer::DoD;
use BSRepServer::SLSA;
use BSDispatcher::Constraints;
use BSCando;

Expand Down Expand Up @@ -4268,6 +4269,15 @@ sub registry_version {
return undef;
}

sub slsa_addrefs {
my ($cgi, $refprpa) = @_;
my $refs = BSUtil::fromstorable(BSServer::read_data(1000000000));
for my $prpa (sort keys %$refs) {
BSRepServer::SLSA::add_references($prpa, $refprpa, $refs->{$prpa});
}
return $BSStdServer::return_ok;
}

sub hello {
my ($cgi) = @_;
my $part = "";
Expand Down Expand Up @@ -4362,6 +4372,9 @@ my $dispatches = [
'GET|HEAD:/registry/_catalog' => \&registry_catalog,
'GET|HEAD:/registry' => \&registry_version,

# slsa
'POST:/slsa cmd=addrefs $prpa' => \&slsa_addrefs,

# configuration
'PUT:/configuration' => \&putconfiguration,
'/configuration' => \&getconfiguration,
Expand Down
67 changes: 66 additions & 1 deletion src/backend/bs_signer
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,67 @@ sub signhelm {
writestr("$jobdir/$chart.prov", undef, $prov_signed);
}

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

sub signslaprovenance {
my ($jobstatus, $signfile, $jobdir, $refprpa, @signargs) = @_;
# for now just do reposerver communication
return unless -e $signfile && -s _ < 1000000000;
my $provenance_json = readstr($signfile);
# not user generated, so we die() on error
my $provenance = JSON::XS::decode_json($provenance_json);
die unless $provenance && ref($provenance) eq 'HASH';
my $alreadysigned;
if ($provenance->{'payload'}) {
$alreadysigned = 1;
die("bad payload type\n") unless $provenance->{'payloadType'} eq 'application/vnd.in-toto+json';
$provenance_json = MIME::Base64::decode_base64($provenance->{'payload'});
$provenance = JSON::XS::decode_json($provenance_json);
}
die("no predicate in provenance file?\n") unless ref($provenance->{'predicate'}) eq 'HASH';

# pin all binaries from the materials
my $materials = $provenance->{'predicate'}->{'materials'};
die("no materials in provenance file?\n") unless ref($materials) eq 'ARRAY';
my %todo;
for my $material (@$materials) {
die("bad material in provenance file?\n") unless ref($material) eq 'HASH';
die("bad material uri in provenance file?\n") unless $material->{'uri'};
next unless $material->{'uri'} =~ /^(https?:.*)\/build\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(?:[^\/]+)\/([^\/]+)$/s;
my $server = $1;
my $prpa = "$2/$3/$4";
my $bin = $5;
die("material with bad digest\n") unless ref($material->{'digest'}) eq 'HASH' && $material->{'digest'}->{'sha256'};
$todo{$prpa}->{$material->{'digest'}->{'sha256'}} = $bin;
}
my $param = {
'uri' => "$BSConfig::srcserver/slsa",
'request' => 'POST',
'data' => BSUtil::tostorable(\%todo),
};
eval { BSRPC::rpc($param, undef, "cmd=addrefs", "prpa=$refprpa") };
$jobstatus->{'result'} = 'rebuild' if $@ && $@ =~ /^404/; # rebuild if a binary is missing
die($@) if $@;

# now sign the provenance statement
if (!$alreadysigned) {
# hack: prepend _ to payloadType so it comes first
my $envelope = {
'_payloadType' => 'application/vnd.in-toto+json',
'payload' => MIME::Base64::encode_base64($provenance_json, ''),
};
my $dsse = dsse_pae($envelope->{'_payloadType'}, $envelope->{'payload'});
my $sig = BSUtil::xsystem($dsse, $BSConfig::sign, @signargs, '-D');
push @{$envelope->{'signatures'}}, {'sig' => MIME::Base64::encode_base64($sig)};
my $envelope_json = JSON::XS->new->utf8->canonical->encode($envelope);;
$envelope_json =~ s/_payloadType/payloadType/;
writestr("$jobdir/.slsa_provenance.sIgN$$", $signfile, $envelope_json);
}
}

sub fixup_sha256_checksum {
my ($jobdir, $shafile, $isofile) = @_;
return if ((-s "$jobdir/$shafile") || 0) > 65536;
Expand Down Expand Up @@ -614,7 +675,7 @@ sub signjob {
my $projid = $info->{'project'};
$prpa_stats = "$projid/$info->{'repository'}/$arch";
my @files = sort(ls($jobdir));
my @signfiles = grep {/\.(?:d?rpm|sha256|iso|pkg\.tar\.gz|pkg\.tar\.xz|pkg\.tar\.zst|rsasign|AppImage|appx|helminfo)$/} @files;
my @signfiles = grep {$_ eq '_slsa_provenance_stmt.json' || /\.(?:d?rpm|sha256|iso|pkg\.tar\.gz|pkg\.tar\.xz|pkg\.tar\.zst|rsasign|AppImage|appx|helminfo)$/} @files;
my $needpubkey;
if (grep {$_ eq '.kiwitree_tosign'} @files) {
for my $f (split("\n", readstr("$jobdir/.kiwitree_tosign"))) {
Expand Down Expand Up @@ -662,6 +723,10 @@ sub signjob {

eval {
for my $signfile (@signfiles) {
if ($signfile eq '_slsa_provenance_stmt.json') {
signslaprovenance($jobstatus, "$jobdir/$signfile", $jobdir, "$projid/$info->{'repository'}/$info->{'arch'}", @signargs);
next;
}
if ($signfile =~ /\.helminfo$/) {
signhelm($jobstatus, "$jobdir/$signfile", $jobdir, @signargs);
next;
Expand Down
31 changes: 31 additions & 0 deletions src/backend/bs_srcserver
Original file line number Diff line number Diff line change
Expand Up @@ -7305,6 +7305,34 @@ sub sigstore_forward {

####################################################################

sub slsa_addrefs {
my ($cgi, $refprpa) = @_;
my $refs = BSUtil::fromstorable(BSServer::read_data(1000000000));
my %servers;
for my $prpa (sort keys %$refs) {
my ($projid) = split('/', $prpa);
my $proj = BSRevision::readproj_local($projid, 1);
$proj = BSSrcServer::Remote::remoteprojid($projid) if !$proj || $proj->{'remoteurl'};
if ($proj->{'remoteurl'}) {
# skip interconnect references for now
next;
}
my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
$servers{$reposerver}->{$prpa} = $refs->{$prpa};
}
for my $server (sort keys %servers) {
my $param = {
'uri' => "$server/slsa",
'request' => 'POST',
'data' => BSUtil::tostorable($servers{$server}),
};
BSRPC::rpc($param, undef, "cmd=addrefs", "prpa=$refprpa");
}
return $BSStdServer::return_ok;
}

####################################################################

sub run {
my ($conf) = @_;
BSSrcServer::SQLite::init_publisheddb($extrepodb) if $BSConfig::published_db_sqlite;
Expand Down Expand Up @@ -7516,6 +7544,9 @@ my $dispatches = [
'GET|HEAD:/registry' => \&registry_version,
'GET|HEAD:/sigstore/$...:regrepo/$sig:' => \&sigstore_forward,

# slsa
'POST:/slsa cmd=addrefs $prpa' => \&slsa_addrefs,

'/ajaxstatus' => \&getajaxstatus,
'/serverstatus' => \&BSStdServer::serverstatus,
];
Expand Down

0 comments on commit 3a5033d

Please sign in to comment.