From f466135abc563fca56534e3796d175bd82cc2e15 Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Wed, 27 May 2015 21:39:07 +0100 Subject: Infra: New approach to reply prevention hhen NOFFCHECK, involving removed tags file --- infra/dgit-repos-server | 161 +++++++++++++++++++++++++++--------------------- 1 file changed, 92 insertions(+), 69 deletions(-) (limited to 'infra') 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 () { + 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:". -- cgit v1.2.3