;
last if !defined;
}
T->error and die $!;
close PT or die $!;
close DS or die $!;
printdebug " parsetag ok.\n";
}
sub checksig_keyring ($) {
my ($keyringfile) = @_;
# returns primary-keyid if signed by a key in this keyring
# or undef if not
# or dies on other errors
my $ok = undef;
printdebug " checksig keyring $keyringfile...\n";
our @cmd = (qw(gpgv --status-fd=1 --keyring),
$keyringfile,
qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
debugcmd '|',@cmd;
open P, "-|", @cmd
or die $!;
while () {
next unless s/^\[GNUPG:\] //;
chomp or die;
printdebug " checksig| $_\n";
my @l = split / /, $_;
if ($l[0] eq 'NO_PUBKEY') {
last;
} elsif ($l[0] eq 'VALIDSIG') {
my $sigtype = $l[9];
$sigtype eq '00' or reject "signature is not of type 00!";
$ok = $l[10];
die unless defined $ok;
last;
}
}
close P;
printdebug sprintf " checksig ok=%d\n", !!$ok;
return $ok;
}
sub dm_txt_check ($$) {
my ($keyid, $dmtxtfn) = @_;
printdebug " dm_txt_check $keyid $dmtxtfn\n";
open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
while (
) {
m/^fingerprint:\s+\Q$keyid\E$/oi
..0 or next;
if (s/^allow:/ /i..0) {
} else {
m/^./
or reject "key $keyid missing Allow section in permissions!";
next;
}
# in right stanza...
s/^[ \t]+//
or reject "package $package not allowed for key $keyid";
# in allow field...
s/\([^()]+\)//;
s/\,//;
chomp or die;
printdebug " dm_txt_check allow| $_\n";
foreach my $p (split /\s+/) {
if ($p eq $package) {
# yay!
printdebug " dm_txt_check ok\n";
return;
}
}
}
DT->error and die $!;
close DT or die $!;
reject "key $keyid not in permissions list although in keyring!";
}
sub verifytag () {
foreach my $kas (split /:/, $keyrings) {
printdebug "verifytag $kas...\n";
$kas =~ s/^([^,]+),// or die;
my $keyid = checksig_keyring $1;
if (defined $keyid) {
if ($kas =~ m/^a$/) {
printdebug "verifytag a ok\n";
return; # yay
} elsif ($kas =~ m/^m([^,]+)$/) {
dm_txt_check($keyid, $1);
printdebug "verifytag m ok\n";
return;
} else {
die;
}
}
}
reject "key not found in keyrings";
}
sub suite_is_in ($) {
my ($sf) = @_;
printdebug "suite_is_in ($sf)\n";
if (!open SUITES, "<", $sf) {
$!==ENOENT or die $!;
return 0;
}
while () {
chomp;
next unless m/\S/;
next if m/^\#/;
s/\s+$//;
return 1 if $_ eq $suite;
}
die $! if SUITES->error;
return 0;
}
sub checksuite () {
printdebug "checksuite ($suitesfile)\n";
return if suite_is_in $suitesfile;
reject "unknown suite";
}
sub checktagnoreplay () {
# We need to prevent a replay attack using an earlier signed tag.
# We also want to archive in the history the object ids of
# anything we remove, even if we get rid of the actual objects.
#
# So, we check that the signed tag mentions the name and tag
# object id of:
#
# (a) In the case of FRESHREPO: all tags and refs/heads/* in
# the repo. That is, effectively, all the things we are
# deleting.
#
# This prevents any tag implying a FRESHREPO push
# being replayed into a different state of the repo.
#
# There is still the folowing risk: If a non-ff push is of a
# head which is an ancestor of a previous ff-only push, the
# previous push can be replayed.
#
# So we keep a separate list, as a file in the repo, of all
# the tag object ids we have ever seen and removed. Any such
# tag object id will be rejected even for ff-only pushes.
#
# (b) In the case of just NOFFCHECK: all tags referring to the
# current head for the suite (there must be at least one).
#
# This prevents any tag implying a NOFFCHECK push being
# replayed to rewind from a different head.
#
# The possibility of an earlier ff-only push being replayed is
# eliminated as follows: the tag from such a push would still
# be in our repo, and therefore the replayed push would be
# rejected because the set of refs being updated would be
# wrong.
if (!open PREVIOUS, "<", removedtagsfile) {
die removedtagsfile." $!" unless $!==ENOENT;
} else {
# Protocol for updating this file is to append to it, not
# write-new-and-rename. So all updates are prefixed with \n
# and suffixed with " .\n" so that partial writes can be
# ignored.
while () {
next unless m/^(\w+) (.*) \.\n/;
next unless $1 eq $tagval;
reject "Replay of previously-rewound upload ($tagval $2)";
}
die removedtagsfile." $!" if PREVIOUS->error;
close PREVIOUS;
}
return unless $policy & (FRESHREPO|NOFFCHECK);
my $garbagerepo = "$dgitrepos/${package}_garbage";
lockrealtree();
my $nchecked = 0;
my @problems;
my $check_ref_previously= sub {
my ($objid,$objtype,$fullrefname,$reftail) = @_;
my $supkey = $fullrefname;
$supkey =~ s{^refs/}{} or die "$supkey $objid ?";
my $supobjid = $previously{$supkey};
if (!defined $supobjid) {
printdebug "checktagnoreply - missing\n";
push @problems, "does not declare previously $supkey";
} elsif ($supobjid ne $objid) {
push @problems, "declared previously $supkey=$supobjid".
" but actually previously $supkey=$objid";
} else {
$nchecked++;
}
};
if ($policy & FRESHREPO) {
foreach my $kind (qw(tags heads)) {
git_for_each_ref("refs/$kind", $check_ref_previously);
}
} else {
my $branch= server_branch($suite);
my $branchhead= git_get_ref(server_ref($suite));
if (!length $branchhead) {
# No such branch - NOFFCHECK was unnecessary. Oh well.
printdebug "checktagnoreplay - not FRESHREPO, new branch, ok\n";
} else {
printdebug "checktagnoreplay - not FRESHREPO,".
" checking for overwriting refs/$branch=$branchhead\n";
git_for_each_tag_referring($branchhead, sub {
my ($tagobjid,$refobjid,$fullrefname,$tagname) = @_;
$check_ref_previously->($tagobjid,undef,$fullrefname,undef);
});
printdebug "checktagnoreplay - not FRESHREPO, nchecked=$nchecked";
push @problems, "does not declare previously any tag".
" referring to branch head $branch=$branchhead"
unless $nchecked;
}
}
if (@problems) {
reject "replay attack prevention check failed:".
" signed tag for $version: ".
join("; ", @problems).
"\n";
}
printdebug "checktagnoreplay - all ok ($tagval)\n"
}
sub tagh1 ($) {
my ($tag) = @_;
my $vals = $tagh{$tag};
reject "missing header $tag in signed tag object" unless $vals;
reject "multiple headers $tag in signed tag object" unless @$vals == 1;
return $vals->[0];
}
sub checks () {
printdebug "checks\n";
tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
tagh1('object') eq $commit or reject "tag refers to wrong commit";
tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
my @expecttagnames = debiantags($version, $distro);
printdebug "expected tag @expecttagnames\n";
grep { $tagname eq $_ } @expecttagnames or die;
foreach my $othertag (grep { $_ ne $tagname } @expecttagnames) {
reject "tag $othertag (pushed with differing dgit version)".
" already exists -".
" not replacing previously-pushed version"
if git_get_ref "refs/tags/".$othertag;
}
lockrealtree();
@policy_args = ($package,$version,$suite,$tagname,
join(",",@deliberatelies));
$policy = policyhook(NOFFCHECK|FRESHREPO|NOCOMMITCHECK, 'push', @policy_args);
if (defined $tagexists_error) {
if ($policy & FRESHREPO) {
printdebug "ignoring tagexists_error: $tagexists_error\n";
} else {
reject $tagexists_error;
}
}
checktagnoreplay();
checksuite();
# check that our ref is being fast-forwarded
printdebug "oldcommit $oldcommit\n";
if (!($policy & NOFFCHECK) && $oldcommit =~ m/[^0]/) {
$?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
chomp $mb;
$mb eq $oldcommit or reject "not fast forward on dgit branch";
}
# defend against commits generated by #849041
if (!($policy & NOCOMMITCHECK)) {
my @checks = qw(%at
%ct);
my @chk = qw(git log -z);
push @chk, '--pretty=tformat:%H%n'.
(join "", map { $_, '%n' } @checks);
push @chk, "^$oldcommit" if $oldcommit =~ m/[^0]/;
push @chk, $commit;;
printdebug " ~NOCOMMITCHECK @chk\n";
open CHK, "-|", @chk or die $!;
local $/ = "\0";
while () {
next unless m/^$/m;
m/^\w+(?=\n)/ or die;
reject "corrupted object $& (missing metadata)";
}
$!=0; $?=0; close CHK or $?==256 or die "$? $!";
}
if ($policy & FRESHREPO) {
# It's a bit late to be discovering this here, isn't it ?
#
# What we do is: Generate a fresh destination repo right now,
# and arrange to treat it from now on as if it were a
# prospective repo.
#
# The presence of this fresh destination repo is detected by
# the parent, which responds by making a fresh master repo
# from the template. (If the repo didn't already exist then
# $destrepo was _prospective, and we change it here. This is
# OK because the parent's check for _fresh persuades it not to
# use _prospective.)
#
$destrepo = "${workrepo}_fresh"; # workrepo lock covers
mkrepo_fromtemplate $destrepo;
}
}
sub onwardpush () {
my @cmdbase = (qw(git send-pack), $destrepo);
push @cmdbase, qw(--force) if $policy & NOFFCHECK;
if ($ENV{GIT_QUARANTINE_PATH}) {
my $recv_wrapper = "$ENV{GIT_QUARANTINE_PATH}/dgit-recv-wrapper";
mkscript $recv_wrapper, <<'END';
#!/bin/sh
set -e
unset GIT_QUARANTINE_PATH
exec git receive-pack "$@"
END
push @cmdbase, "--receive-pack=$recv_wrapper";
}
my @cmd = @cmdbase;
push @cmd, "$commit:refs/dgit/$suite",
"$tagval:refs/tags/$tagname";
push @cmd, "$maint_tagval:refs/tags/$maint_tagname"
if defined $maint_tagname;
debugcmd '+',@cmd;
$!=0;
my $r = system @cmd;
!$r or die "onward push to $destrepo failed: $r $!";
if (suite_is_in $suitesformasterfile) {
@cmd = @cmdbase;
push @cmd, "$commit:refs/heads/master";
debugcmd '+', @cmd;
$!=0; my $r = system @cmd;
# tolerate errors (might be not ff)
!($r & ~0xff00) or die
"onward push to $destrepo#master failed: $r $!";
}
}
sub finalisepush () {
if ($destrepo eq realdestrepo) {
policyhook(0, 'push-confirm', @policy_args, '');
onwardpush();
} else {
# We are to receive the push into a new repo (perhaps
# because the policy push hook asked us to with FRESHREPO, or
# perhaps because the repo didn't exist before).
#
# We want to provide the policy push-confirm hook with a repo
# which looks like the one which is going to be installed.
# The working repo is no good because it might contain
# previous history.
#
# So we push the objects into the prospective new repo right
# away. If the hook declines, we decline, and the prospective
# repo is never installed.
onwardpush();
policyhook(0, 'push-confirm', @policy_args, $destrepo);
}
}
sub stunthook () {
printdebug "stunthook in $workrepo\n";
chdir $workrepo or die "chdir $workrepo: $!";
mkdir "dgit-tmp" or $!==EEXIST or die $!;
readupdates();
parsetag();
verifytag();
checks();
finalisepush();
printdebug "stunthook done.\n";
}
#----- git-upload-pack -----
sub fixmissing__git_upload_pack () {
$destrepo = "$dgitrepos/_empty";
my $lfh = locksometree($destrepo);
return if stat_exists $destrepo;
rmtree "$destrepo.new";
mkemptyrepo "$destrepo.new", "0644";
rename "$destrepo.new", $destrepo or die $!;
unlink "$destrepo.lock" or die $!;
close $lfh;
}
sub main__git_upload_pack () {
my $lfh = locksometree($destrepo);
printdebug "git-upload-pack in $destrepo\n";
chdir $destrepo or die "$destrepo: $!";
close $lfh;
runcmd qw(git upload-pack), ".";
}
#----- arg parsing and main program -----
sub argval () {
die unless @ARGV;
my $v = shift @ARGV;
die if $v =~ m/^-/;
return $v;
}
our %indistrodir = (
# keys are used for DGIT_DRS_XXX too
'repos' => \$dgitrepos,
'suites' => \$suitesfile,
'suites-master' => \$suitesformasterfile,
'policy-hook' => \$policyhook,
'mirror-hook' => \$mirrorhook,
'dgit-live' => \$dgitlive,
);
our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook
mirrorhook dgitlive keyrings dgitrepos distrodir);
# workrepo and destrepo handled ad-hoc
sub mode_ssh () {
die if @ARGV;
my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
$cmd =~ m{
^
(?: \S* / )?
( [-0-9a-z]+ )
\s+
'? (?: \S* / )?
($package_re) \.git
'?$
}ox
or reject "command string not understood";
my $method = $1;
$package = $2;
my $funcn = $method;
$funcn =~ y/-/_/;
my $mainfunc = $main::{"main__$funcn"};
reject "unknown method" unless $mainfunc;
policy_checkpackage();
if (stat_exists realdestrepo) {
$destrepo = realdestrepo;
} else {
printdebug " fixmissing $funcn\n";
my $fixfunc = $main::{"fixmissing__$funcn"};
&$fixfunc;
}
printdebug " running main $funcn\n";
&$mainfunc;
}
sub mode_cron () {
die if @ARGV;
my $listfh = tempfile();
open STDOUT, ">&", $listfh or die $!;
policyhook(0,'check-list');
open STDOUT, ">&STDERR" or die $!;
seek $listfh, 0, 0 or die $!;
while (<$listfh>) {
chomp or die;
next if m/^\s*\#/;
next unless m/\S/;
die unless m/^($package_re)$/;
$package = $1;
policy_checkpackage();
}
die $! if $listfh->error;
}
sub parseargsdispatch () {
die unless @ARGV;
delete $ENV{'GIT_DIR'}; # if not run via ssh, our parent git process
delete $ENV{'GIT_PREFIX'}; # sets these and they mess things up
if ($ENV{'DGIT_DRS_DEBUG'}) {
enabledebug();
}
if ($ARGV[0] eq '--pre-receive-hook') {
if ($debuglevel) {
$debugprefix.="=";
printdebug "in stunthook ".(shellquote @ARGV)."\n";
foreach my $k (sort keys %ENV) {
printdebug "$k=$ENV{$k}\n" if $k =~ m/^DGIT/;
}
}
shift @ARGV;
@ARGV == 1 or die;
$package = shift @ARGV;
${ $main::{$_} } = $ENV{"DGIT_DRS_\U$_"} foreach @hookenvs;
defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
open STDOUT, ">&STDERR" or die $!;
eval {
stunthook();
};
if ($@) {
recorderror "$@" or die;
die $@;
}
exit 0;
}
$distro = argval();
$distrodir = argval();
$keyrings = argval();
foreach my $dk (keys %indistrodir) {
${ $indistrodir{$dk} } = "$distrodir/$dk";
}
while (@ARGV && $ARGV[0] =~ m/^--([-0-9a-z]+)=/ && $indistrodir{$1}) {
${ $indistrodir{$1} } = $'; #';
shift @ARGV;
}
$ENV{"DGIT_DRS_\U$_"} = ${ $main::{$_} } foreach @hookenvs;
die unless @ARGV==1;
my $mode = shift @ARGV;
die unless $mode =~ m/^--(\w+)$/;
my $fn = ${*::}{"mode_$1"};
die unless $fn;
$fn->();
}
sub unlockall () {
while (my $fh = pop @lockfhs) { close $fh; }
}
sub cleanup () {
unlockall();
if (!chdir "$dgitrepos/_tmp") {
$!==ENOENT or die $!;
return;
}
foreach my $lf (<*.lock>) {
my $tree = $lf;
$tree =~ s/\.lock$//;
next unless acquirermtree($tree, 0);
remove $lf or warn $!;
unlockall();
}
}
parseargsdispatch();
cleanup();