;
@@ -401,7 +619,7 @@ sub parsetag () {
T->error and die $!;
close PT or die $!;
close DS or die $!;
- debug " parsetag ok.";
+ printdebug " parsetag ok.\n";
}
sub checksig_keyring ($) {
@@ -412,12 +630,12 @@ sub checksig_keyring ($) {
my $ok = undef;
- debug " checksig keyring $keyringfile...";
+ printdebug " checksig keyring $keyringfile...\n";
our @cmd = (qw(gpgv --status-fd=1 --keyring),
$keyringfile,
qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
- debugcmd @cmd;
+ debugcmd '|',@cmd;
open P, "-|", @cmd
or die $!;
@@ -425,7 +643,7 @@ sub checksig_keyring ($) {
while () {
next unless s/^\[GNUPG:\] //;
chomp or die;
- debug " checksig| $_";
+ printdebug " checksig| $_\n";
my @l = split / /, $_;
if ($l[0] eq 'NO_PUBKEY') {
last;
@@ -439,17 +657,17 @@ sub checksig_keyring ($) {
}
close P;
- debug sprintf " checksig ok=%d", !!$ok;
+ printdebug sprintf " checksig ok=%d\n", !!$ok;
return $ok;
}
sub dm_txt_check ($$) {
my ($keyid, $dmtxtfn) = @_;
- debug " dm_txt_check $keyid $dmtxtfn";
+ printdebug " dm_txt_check $keyid $dmtxtfn\n";
open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
while (
) {
- m/^fingerprint:\s+$keyid$/oi
+ m/^fingerprint:\s+\Q$keyid\E$/oi
..0 or next;
if (s/^allow:/ /i..0) {
} else {
@@ -464,11 +682,11 @@ sub dm_txt_check ($$) {
s/\([^()]+\)//;
s/\,//;
chomp or die;
- debug " dm_txt_check allow| $_";
+ printdebug " dm_txt_check allow| $_\n";
foreach my $p (split /\s+/) {
if ($p eq $package) {
# yay!
- debug " dm_txt_check ok";
+ printdebug " dm_txt_check ok\n";
return;
}
}
@@ -480,16 +698,16 @@ sub dm_txt_check ($$) {
sub verifytag () {
foreach my $kas (split /:/, $keyrings) {
- debug "verifytag $kas...";
+ printdebug "verifytag $kas...\n";
$kas =~ s/^([^,]+),// or die;
my $keyid = checksig_keyring $1;
if (defined $keyid) {
if ($kas =~ m/^a$/) {
- debug "verifytag a ok";
+ printdebug "verifytag a ok\n";
return; # yay
} elsif ($kas =~ m/^m([^,]+)$/) {
dm_txt_check($keyid, $1);
- debug "verifytag m ok";
+ printdebug "verifytag m ok\n";
return;
} else {
die;
@@ -499,20 +717,138 @@ sub verifytag () {
reject "key not found in keyrings";
}
-sub checksuite () {
- debug "checksuite ($suitesfile)";
- open SUITES, "<", $suitesfile or die $!;
+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 if $_ eq $suite;
+ 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};
@@ -522,52 +858,163 @@ sub tagh1 ($) {
}
sub checks () {
- debug "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 $v = $version;
- $v =~ y/~:/_%/;
+ 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;
+ }
- debug "translated version $v";
- $tagname eq "debian/$v" or die;
+ lockrealtree();
- my ($policy) = policyhook(NOFFCHECK, 'push',$package,
- $version,$suite,$tagname,
- join(",",@delberatelies));
+ @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
- debug "oldcommit $oldcommit";
+ 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(%ae %at
+ %ce %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 () {
- debug "stunthook";
+ printdebug "stunthook in $workrepo\n";
chdir $workrepo or die "chdir $workrepo: $!";
mkdir "dgit-tmp" or $!==EEXIST or die $!;
readupdates();
parsetag();
verifytag();
checks();
- onwardpush();
- debug "stunthook done.";
+ finalisepush();
+ printdebug "stunthook done.\n";
}
#----- git-upload-pack -----
sub fixmissing__git_upload_pack () {
$destrepo = "$dgitrepos/_empty";
- my $lfh = acquiretree($destrepo,1);
- return if stat $destrepo;
- die $! unless $!==ENOENT;
+ my $lfh = locksometree($destrepo);
+ return if stat_exists $destrepo;
rmtree "$destrepo.new";
mkemptyrepo "$destrepo.new", "0644";
rename "$destrepo.new", $destrepo or die $!;
@@ -576,7 +1023,11 @@ sub fixmissing__git_upload_pack () {
}
sub main__git_upload_pack () {
- runcmd qw(git upload-pack), $destrepo;
+ 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 -----
@@ -588,46 +1039,23 @@ sub argval () {
return $v;
}
-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'}) {
- $debug='=';
- open DEBUG, ">&STDERR" or die $!;
- }
+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,
+ );
- if ($ARGV[0] eq '--pre-receive-hook') {
- if ($debug) { $debug.="="; }
- shift @ARGV;
- @ARGV == 1 or die;
- $package = shift @ARGV;
- defined($distro = $ENV{'DGIT_DRS_DISTRO'}) or die;
- defined($suitesfile = $ENV{'DGIT_DRS_SUITES'}) or die;
- defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
- defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
- defined($keyrings = $ENV{'DGIT_DRS_KEYRINGS'}) or die $!;
- defined($policyhook = $ENV{'DGIT_DRS_POLICYHOOK'}) or die $!;
- open STDOUT, ">&STDERR" or die $!;
- eval {
- stunthook();
- };
- if ($@) {
- recorderror "$@" or die;
- die $@;
- }
- exit 0;
- }
+our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook
+ mirrorhook dgitlive keyrings dgitrepos distrodir);
- $ENV{'DGIT_DRS_DISTRO'} = argval();
- $ENV{'DGIT_DRS_SUITES'} = argval();
- $ENV{'DGIT_DRS_KEYRINGS'} = argval();
- $dgitrepos = argval();
- $ENV{'DGIT_DRS_POLICYHOOK'} = $policyhook = argval();
+# workrepo and destrepo handled ad-hoc
- die unless @ARGV==1 && $ARGV[0] eq '--ssh';
+sub mode_ssh () {
+ die if @ARGV;
my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
$cmd =~ m{
@@ -642,7 +1070,6 @@ sub parseargsdispatch () {
or reject "command string not understood";
my $method = $1;
$package = $2;
- $realdestrepo = "$dgitrepos/$package.git";
my $funcn = $method;
$funcn =~ y/-/_/;
@@ -650,25 +1077,100 @@ sub parseargsdispatch () {
reject "unknown method" unless $mainfunc;
- my ($policy, $pollock) = policyhook(FRESHREPO,'check-package',$package);
- if ($policy & FRESHREPO) {
- movetogarbage;
- }
- close $pollock or die $!;
+ policy_checkpackage();
- if (stat $realdestrepo) {
- $destrepo = $realdestrepo;
+ if (stat_exists realdestrepo) {
+ $destrepo = realdestrepo;
} else {
- $! == ENOENT or die "stat dest repo $destrepo: $!";
- debug " fixmissing $funcn";
+ printdebug " fixmissing $funcn\n";
my $fixfunc = $main::{"fixmissing__$funcn"};
&$fixfunc;
}
- debug " running main $funcn";
+ 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; }
}
@@ -682,7 +1184,7 @@ sub cleanup () {
foreach my $lf (<*.lock>) {
my $tree = $lf;
$tree =~ s/\.lock$//;
- next unless acquiretree($tree, 0);
+ next unless acquirermtree($tree, 0);
remove $lf or warn $!;
unlockall();
}