summaryrefslogtreecommitdiff
path: root/infra/dgit-repos-server
diff options
context:
space:
mode:
authorIan Jackson <ijackson@chiark.greenend.org.uk>2015-05-27 21:39:07 +0100
committerIan Jackson <ijackson@chiark.greenend.org.uk>2015-05-31 12:58:16 +0100
commitf466135abc563fca56534e3796d175bd82cc2e15 (patch)
treeca47bc50536137704d9bbd82cf6e08d54497f7c8 /infra/dgit-repos-server
parent2f611c6de222547a01d81d50dc5cfecb9fb7d277 (diff)
Infra: New approach to reply prevention hhen NOFFCHECK, involving removed tags file
Diffstat (limited to 'infra/dgit-repos-server')
-rwxr-xr-xinfra/dgit-repos-server161
1 files changed, 92 insertions, 69 deletions
diff --git a/infra/dgit-repos-server b/infra/dgit-repos-server
index f2f3088..1fcc1fd 100755
--- a/infra/dgit-repos-server
+++ b/infra/dgit-repos-server
@@ -294,10 +294,18 @@ sub mkrepo_fromtemplate ($) {
sub movetogarbage () {
# realdestrepo must have been locked
+
+ ensuredir "$dgitrepos/_removed-tags";
+ open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!";
+ git_for_each_ref(debiantag('*'), sub {
+ my ($objid,$objtype,$fullrefname,$reftail) = @_;
+ print PREVIOUS "\n$objid $reftail .\n" or die $!;
+ });
+ close PREVIOUS or die $!;
+
my $garbagerepo = "$dgitrepos/${package}_garbage";
- # We arrange to always keep at least one old tree, for anti-rewind
- # purposes (and, I guess, recovery from mistakes). This is either
- # $garbage or $garbage-old.
+ # We arrange to always keep at least one old tree, for recovery
+ # from mistakes. This is either $garbage or $garbage-old.
if (stat_exists "$garbagerepo") {
printdebug "movetogarbage: rmtree $garbagerepo-tmp\n";
rmtree "$garbagerepo-tmp";
@@ -633,87 +641,102 @@ sub checksuite () {
sub checktagnoreplay () {
# We need to prevent a replay attack using an earlier signed tag.
- # We also want to archive in the history anything
+ # 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:
#
- # 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.
#
- # (a) In the case of FRESHREPO all tags and refs/heads/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.
+ # This prevents any tag implying a FRESHREPO push
+ # being replayed into a different state of the repo.
#
- # (b) In the case of just NOFFCHECK all tags referring to
- # the current head for the suite (there must be at least one).
- # This guarantees that the
+ # 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 (<PREVIOUS>) {
+ 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();
- local $ENV{GIT_DIR};
- foreach my $garb ("$garbagerepo", "$garbagerepo-old") {
- if (stat_exists $garb) {
- $ENV{GIT_DIR} = $garb;
- last;
+ my $nchecked = 0;
+ my @problems;
+
+ my $check_ref_superseded= sub {
+ my ($objid,$objtype,$fullrefname,$reftail) = @_;
+ my $supkey = $fullrefname;
+ $supkey =~ s{^refs/}{} or die "$supkey $objid ?";
+ my $supobjid = $supersedes{$supkey};
+ if (!defined $supobjid) {
+ printdebug "checktagnoreply - missing\n";
+ push @problems, "does not supersede $supkey";
+ } elsif ($supobjid ne $objid) {
+ push @problems, "supersedes $supkey=$supobjid".
+ " but previously $supkey=$objid";
+ } else {
+ $nchecked++;
}
- }
- if (!defined $ENV{GIT_DIR}) {
- # Nothing to overwrite so the FRESHREPO and NOFFCHECK were
- # pointless. Oh well.
- printdebug "checktagnoreplay - no garbage, ok\n";
- return;
- }
+ };
- my $onlyreferring;
- if (!($policy & FRESHREPO)) {
- my $branch = server_branch($suite);
- $!=0; $?=0; $_ =
- `git for-each-ref --format='%(objectname)' '[r]efs/$branch'`;
- defined or die "$branch $? $!";
- $? and die "$branch $?";
- if (!length) {
+ if ($policy & FRESHREPO) {
+ foreach my $kind (qw(tags heads)) {
+ git_for_each_ref("refs/$kind", $check_ref_superseded);
+ }
+ } else {
+ my $branch= server_branch($suite);
+ my $branchhead= git_get_ref($branch);
+ if (!length $branchhead) {
# No such branch - NOFFCHECK was unnecessary. Oh well.
printdebug "checktagnoreplay - not FRESHREPO, new branch, ok\n";
- return;
- }
- m/^(\w+)\n$/ or die "$branch $_ ?";
- $onlyreferring = $1;
- printdebug "checktagnoreplay - not FRESHREPO,".
- " checking for overwriting refs/$branch=$onlyreferring\n";
- }
-
- my @problems;
-
- git_for_each_tag_referring($onlyreferring, sub {
- my ($tagobjid,$refobjid,$fullrefname,$tagname) = @_;
- printdebug "checktagnoreplay - overwriting".
- " $fullrefname=$tagobjid->$refobjid\n";
- my $supers = $supersedes{$fullrefname};
- if (!defined $supers) {
- printdebug "checktagnoreply - fallbacks\n";
- my $super_fallback = 0;
- foreach my $didsuper (sort keys %supersedes) {
- my $didsuper_tagobjid = $supersedes{$didsuper};
- my $didsuper_refobjid = git_rev_parse $didsuper_tagobjid;
- printdebug "checktagnoreply - fallback".
- " $didsuper=$didsuper_refobjid->$didsuper_tagobjid\n";
- last if
- $refobjid ne $didsuper_refobjid
- and is_fast_fwd($refobjid, $didsuper_refobjid);
- printdebug "checktagnoreply - fallback $didsuper OK\n";
- $super_fallback = 1;
- }
- push @problems, "does not supersede $fullrefname"
- unless $super_fallback;
- } elsif ($supers ne $tagobjid) {
- push @problems,
- "supersedes $fullrefname=$supers but previously $fullrefname=$tagobjid";
} else {
- # ok;
+ 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_superseded->($tagobjid,undef,$fullrefname,undef);
+ });
+ printdebug "checktagnoreply - not FRESHREPO, nchecked=$nchecked";
+ push @problems, "does not supersede any tag".
+ " referring to branch head $branch=$branchhead"
+ unless $nchecked;
}
- });
+ }
if (@problems) {
reject "replay attack prevention check failed:".