From 4487fc3517fb6db0dd8f25199cc0654c026d203d Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Sat, 21 Mar 2015 14:56:24 +0000 Subject: Support --deliberately-not-fast-forward --- dgit | 25 ++++++++++++- infra/dgit-repos-server | 93 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/dgit b/dgit index ad6289d..9d40cee 100755 --- a/dgit +++ b/dgit @@ -1572,6 +1572,12 @@ tagger $authline $package release $cversion for $clogsuite ($csuite) [dgit] [dgit distro=$distro$delibs] END + foreach my $ref (sort keys %supersedes) { + print TO <('.tmp'); @@ -1684,6 +1690,15 @@ sub dopush () { responder_send_command("param head $head"); responder_send_command("param csuite $csuite"); + my $forceflag = deliberately('not-fast-forward') ? '+' : ''; + if ($forceflag && defined $lastpush_hash) { + git_for_each_tag_referring($lastpush_hash, sub { + my ($objid,$fullrefname,$tagname) = @_; + responder_send_command("supersedes $fullrefname=$objid"); + $supersedes{$fullrefname} = $objid; + }); + } + my $tfn = sub { ".git/dgit/tag$_[0]"; }; my $tagobjfn; @@ -1707,7 +1722,7 @@ sub dopush () { create_remote_git_repo(); } runcmd_ordryrun @git, qw(push),access_giturl(), - "HEAD:".rrref(), "refs/tags/$tag"; + $forceflag."HEAD:".rrref(), "refs/tags/$tag"; runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD'; if ($we_are_responder) { @@ -1988,6 +2003,14 @@ sub i_resp_param ($) { $i_param{$1} = $2; } +sub i_resp_supersedes ($) { + $_[0] =~ m#^(refs/tags/\S+)=(\w+)$# + or badproto \*RO, "bad supersedes spec"; + my $r = system qw(git check-ref-format), $1; + die "bad supersedes ref spec ($r)" if $r; + $supersedes{$1} = $2; +} + our %i_wanted; sub i_resp_want ($) { diff --git a/infra/dgit-repos-server b/infra/dgit-repos-server index d2f94f1..551efff 100755 --- a/infra/dgit-repos-server +++ b/infra/dgit-repos-server @@ -103,6 +103,7 @@ our $keyrings; our @lockfhs; our $debug=''; our @deliberatelies; +our %supersedes; our $policy; #----- utilities ----- @@ -211,12 +212,24 @@ sub mkrepo_fromtemplate ($) { } sub movetogarbage () { - my $garbagerepo = "$dgitrepos/_tmp/${package}_garbage"; - acquiretree($garbagerepo,1); - rmtree $garbagerepo; + my $garbagerepo = "$dgitrepos/${package}_garbage"; + my $lfh =acquiretree($garbagerepo,1); + # 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. + if (stat_exists "$garbagerepo") { + rmtree "$garbagerepo-tmp"; + if (rename "$garbagerepo-old", "$garbagerepo-tmp") { + rmtree "$garbagerepo-tmp"; + } else { + die "$garbagerepo $!" unless $!==ENOENT; + } + rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!"; + } rename $realdestrepo, $garbagerepo or $! == ENOENT - or die "rename repo $realdestrepo to $garbagerepo: $!"; + or die "$garbagerepo $!"; + close $lfh; } sub onwardpush () { @@ -391,6 +404,9 @@ sub parsetag () { die "$1 != $distro" unless $1 eq $distro; } elsif (s/^(--deliberately-$package_re) //) { push @deliberatelies, $1; + } elsif (s/^supersede:(\S+)=(\w+) //) { + die "supersede $1 twice" if defined $supersedes{$1}; + $supersedes{$1} = $2; } elsif (s/^[-+.=0-9a-z]\S* //) { } else { die "unknown dgit info in tag"; @@ -520,6 +536,74 @@ sub checksuite () { reject "unknown suite"; } +sub checktagnoreplay () { + # We check that the signed tag mentions the name and value of + # (a) in the case of FRESHREPO all tags in 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 prevents a replay attack using an earlier signed tag. + return unless $policy & (FRESHREPO|NOFFCHECK); + + my $garbagerepo = "$dgitrepos/${package}_garbage"; + acquiretree($garbagerepo,1); + + local $ENV{GIT_DIR}; + foreach my $garb ("$garbagerepo", "$garbagerepo-old") { + if (stat_exists $garb) { + $ENV{GIT_DIR} = $garb; + last; + } + } + if (!defined $ENV{GIT_DIR}) { + # Nothing to overwrite so the FRESHREPO and NOFFCHECK were + # pointless. Oh well. + debug "checktagnoreplay - no garbage, ok"; + 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) { + # No such branch - NOFFCHECK was unnecessary. Oh well. + debug "checktagnoreplay - not FRESHREPO, new branch, ok"; + return; + } + m/^(\w+)\n$/ or die "$branch $_ ?"; + $onlyreferring = $1; + debug "checktagnoreplay - not FRESHREPO,". + " checking for overwriting refs/$branch=$onlyreferring"; + } + + my @problems; + + git_for_each_tag_referring($objreferring, sub { + my ($objid,$fullrefname,$tagname) = @_; + debug "checktagnoreplay - overwriting $fullrefname=$objid"; + my $supers = $supersedes{$fullrefname}; + if (!defined $supers) { + push @problems, "does not supersede $fullrefname"; + } elsif ($supers ne $objid) { + push @problems, + "supersedes $fullrefname=$supers but previously $fullrefname=$objid"; + } else { + # ok; + } + }); + + if (@problems) { + reject "replay attack prevention check failed:". + " signed tag for $version: ". + join("; ", @problems). + "\n"; + } + debug "checktagnoreply - all ok" +} + sub tagh1 ($) { my ($tag) = @_; my $vals = $tagh{$tag}; @@ -545,6 +629,7 @@ sub checks () { $version,$suite,$tagname, join(",",@delberatelies)); + checktagnoreplay(); checksuite(); # check that our ref is being fast-forwarded -- cgit v1.2.3