summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:19:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:19:41 +0000
commita27c8b00ebf173659f22f53ce65679e94e7dfb1b (patch)
tree02c68ec259348b63c6328896aa73265eb7b3d730 /scripts
parentInitial commit. (diff)
downloaddebian-keyring-a27c8b00ebf173659f22f53ce65679e94e7dfb1b.tar.xz
debian-keyring-a27c8b00ebf173659f22f53ce65679e94e7dfb1b.zip
Adding upstream version 2022.12.24.upstream/2022.12.24upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/add-key144
-rwxr-xr-xscripts/check-dm-acl23
-rwxr-xr-xscripts/chk_expiry69
-rwxr-xr-xscripts/clean-keydir146
-rwxr-xr-xscripts/explode-keyring38
-rwxr-xr-xscripts/gpg-diff204
-rwxr-xr-xscripts/mail_expired.rb69
-rwxr-xr-xscripts/move-key145
-rwxr-xr-xscripts/parse-email199
-rwxr-xr-xscripts/parse-git-changelog245
-rwxr-xr-xscripts/parse-gpg-update52
-rwxr-xr-xscripts/process-rt667
-rwxr-xr-xscripts/pull-updates90
-rwxr-xr-xscripts/replace-key178
-rwxr-xr-xscripts/revoke-key52
-rwxr-xr-xscripts/update-key95
-rwxr-xr-xscripts/update-keyrings405
-rwxr-xr-xscripts/update-ldap82
18 files changed, 2903 insertions, 0 deletions
diff --git a/scripts/add-key b/scripts/add-key
new file mode 100755
index 0000000..313719f
--- /dev/null
+++ b/scripts/add-key
@@ -0,0 +1,144 @@
+#!/bin/bash
+
+# Copyright (c) 2008 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+# Adds a new key to a keyring directory
+
+set -e
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: add-key keyfile dir" >&2
+ echo "Or: add-key fingerprint dir" >&2
+ exit 1
+fi
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+if echo -n "$1" | egrep -q '^[[:xdigit:]]{40}$'; then
+ fpr=$1
+ keyserver=${KEYSERVER:=pool.sks-keyservers.net}
+ keyfile=$(mktemp -p $GNUPGHOME newkyXXXXXX)
+ echo "Retrieving key $fpr from keyserver $keyserver"
+ gpg --keyserver $keyserver --recv-key "$fpr"
+ gpg --export "$fpr" > $keyfile
+else
+ keyfile=$(readlink -f "$1") # gpg works better with absolute keyring paths
+fi
+keydir="$2"
+
+basename=$(basename "$keyfile")
+date=`date -R`
+
+if [ -f $keyfile ]; then
+ keyid=$(gpg --with-colons --keyid long --options /dev/null --no-auto-check-trustdb < $keyfile | grep '^pub' | cut -d : -f 5)
+else
+ keyid=${1: -16:16}
+fi
+
+for keyring in *-pgp/ *-gpg/; do
+ if [ -e $keyring/0x$keyid ]; then
+ echo "0x$keyid already exists in $keyring - existing key or error."
+ exit 1
+ fi
+done
+
+# Check we have our keyrings available for checking the signatures
+if [ ! -e output/keyrings/debian-keyring.gpg ]; then
+ make
+fi
+
+if [ -f $keyfile ]; then
+ gpg --quiet --import $keyfile
+else
+ gpg --quiet --keyserver the.earth.li --recv-key $1 || true
+ gpg --quiet --keyserver pgp.mit.edu --recv-key $1 || true
+ gpg --quiet --keyserver keyserver.ubuntu.com --recv-key $1 || true
+ gpg --quiet --keyserver the.earth.li --send-key $1
+fi
+gpg --keyring output/keyrings/debian-keyring.gpg \
+ --keyring output/keyrings/debian-nonupload.gpg --check-sigs \
+ --with-fingerprint --keyid-format 0xlong 0x$keyid | \
+sensible-pager
+
+echo "We want signatures from at least two other DDs."
+echo "If this is a key transition, we also want a signature from the DD's old key."
+echo "Are you sure you want to update this key? (y/n)"
+read n
+
+if ( echo $keydir | egrep -q '^(\./)?debian-keyring-gpg/?$' ); then
+ dest=DD
+elif ( echo $keydir | egrep -q '^(\./)?debian-nonupload-gpg/?$' ); then
+ dest=DN
+elif ( echo $keydir | egrep -q '^(\./)?debian-maintainers-gpg/?$' ); then
+ dest=DM
+fi
+
+if [ "x$n" = "xy" -o "x$n" = "xY" ]; then
+ gpg --no-auto-check-trustdb --options /dev/null \
+ --keyring output/keyrings/debian-keyring.gpg \
+ --keyring output/keyrings/debian-nonupload.gpg \
+ --keyring output/keyrings/debian-maintainers.gpg \
+ --export-options export-clean,no-export-attributes \
+ --export $keyid > $keydir/0x$keyid
+ git add $keydir/0x$keyid
+ echo -n "Enter full name of new key: "
+ read name
+ echo -n 'RT issue ID this change closes, if any: '
+ read rtid
+ if [ "$dest" = DD -o "$dest" = DN ]; then
+ echo -n "Enter Debian login of new key: "
+ read login
+ echo "0x$keyid $name <$login>" >> keyids
+ sort keyids > keyids.$$ && mv keyids.$$ keyids
+ git add keyids
+ fi
+
+ log="Add new $dest key 0x${fpr:24:16} ($name) (RT #$rtid)"
+ VERSION=$(head -1 debian/changelog | awk '{print $2}' | sed 's/[\(\)]//g')
+ RELEASE=$(head -1 debian/changelog | awk '{print $3}' | sed 's/;$//')
+ case $RELEASE in
+ UNRELEASED)
+ dch --multimaint-merge -D UNRELEASED -a "$log"
+ ;;
+ unstable)
+ NEWVER=$(date +%Y.%m.xx)
+ if [ "$VERSION" = "$NEWVER" ]
+ then
+ echo '* Warning: New version and previous released version are'
+ echo " the same: $VERSION. This should not be so!"
+ echo ' Check debian/changelog'
+ fi
+ dch -D UNRELEASED -v $NEWVER "$log"
+ ;;
+ *)
+ echo "Last release $VERSION for unknown distribution «$RELEASE»."
+ echo "Not calling dch, do it manually."
+ ;;
+ esac
+ git add debian/changelog
+
+ cat > git-commit-template <<EOF
+$log
+
+Action: add
+Subject: $name
+Username: $login
+Role: $dest
+Key: $fpr
+Key-type:
+RT-Ticket: $rtid
+Request-signed-by:
+Key-certified-by:
+Details:
+EOF
+
+else
+ echo "Not adding key."
+ exit 1
+fi
diff --git a/scripts/check-dm-acl b/scripts/check-dm-acl
new file mode 100755
index 0000000..e886975
--- /dev/null
+++ b/scripts/check-dm-acl
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# Copyright (c) 2016 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+#
+# Grabs the DM ACL list from ftp-master and ensures that all the
+# active keys are still present. Outputs the last commit involving
+# a missing key and indicates if it has been moved to the DD keyring.
+#
+
+for fp in $(curl -s https://ftp-master.debian.org/dm.txt | awk '/^Fingerprint: / { print $2 }'); do
+ keyid=${fp:24}
+ if [ ! -e debian-maintainers-gpg/0x$keyid -a \
+ ! -e debian-nonupload-gpg/0x$keyid ]; then
+ if [ -e debian-keyring-gpg/0x$keyid ]; then
+ echo "0x$keyid moved to DD keyring."
+ else
+ echo "0x$keyid is missing."
+ fi
+ git log --format=oneline -n 1 -- \
+ debian-maintainers-gpg/0x$keyid
+ fi
+done
diff --git a/scripts/chk_expiry b/scripts/chk_expiry
new file mode 100755
index 0000000..2d6b020
--- /dev/null
+++ b/scripts/chk_expiry
@@ -0,0 +1,69 @@
+#!/usr/bin/perl
+use strict;
+use Date::Calc qw(Today Delta_Days Add_Delta_YM);
+
+my (%conf);
+%conf = (keyrings => ['debian-keyring.gpg', 'debian-nonupload.gpg',
+ 'debian-maintainers.gpg'],
+ basedir => 'output/keyrings',
+ cmd => 'gpg --no-default-keyring --keyring %s/%s --list-key|grep expire[ds]:'
+# basedir => '/tmp',
+# cmd => 'cat %s/%s'
+ );
+
+for my $keyring (@{$conf{keyrings}}) {
+ my ($keys, @expired, @nextmonth, @threemonths);
+ $keys = {};
+ print "============================================================\n";
+ print "Processing keyring: $keyring\n\n";
+ for my $line (query_keyring($keyring)) {
+ my ($key, $y, $m, $d);
+ unless ($line =~ m![ps]ub\s+\d+[RDg]/
+ ([\dABCDEF]{8})
+ \s.+expire[ds]:\s
+ (\d{4})-(\d{2})-(\d{2})!x) {
+ warn "Unrecognized: $line";
+ next;
+ }
+ ($key, $y, $m, $d) = ($1, $2, $3, $4);
+ $keys->{$key} = [$y, $m, $d];
+ }
+ print "\nAlready expired keys:\n";
+ report($keys, [Today()]);
+ print "\nKeys expiring soon (one month from today):\n";
+ report($keys, [Add_Delta_YM(Today(),0,1)], [Today()]);
+ print "\nKeys expiring after a month but within three months:\n";
+ report($keys, [Add_Delta_YM(Today(),0,1)], [Add_Delta_YM(Today(),0,3)]);
+}
+
+sub query_keyring {
+ my ($keyring, $cmd);
+ $keyring = shift;
+ $cmd = sprintf($conf{cmd}, $conf{basedir}, $keyring);
+ return `$cmd`;
+}
+
+# Called with three parameters:
+# - $keys: Hash keyed by keyid, with the expiry date in [y,m,d] form as its
+# value
+# - $before: [y,m,d] form. Keys expiring before this date will be reported
+# - $limit: Optional, [y,m,d] form. Keys expiring before this date will be
+# ignored.
+sub report {
+ my ($keys, $before, $limit, %res);
+ $keys = shift;
+ $before = shift;
+ $limit = shift;
+ for my $key (keys %$keys) {
+ next if Delta_Days(@{$keys->{$key}}, @{$before}) < 0;
+ next if $limit and Delta_Days(@{$keys->{$key}}, @{$limit}) > 0;
+ $res{$key} = {expiry => $keys->{$key},
+ days_to_exp => Delta_Days(Today, @{$keys->{$key}}) };
+ }
+
+ foreach my $key (sort {$res{$a}{days_to_exp} <=> $res{$b}{days_to_exp}}
+ keys %res) {
+ printf("%s: %s (%s days)\n", $key, join('-', @{$res{$key}{expiry}}),
+ $res{$key}{days_to_exp});
+ }
+}
diff --git a/scripts/clean-keydir b/scripts/clean-keydir
new file mode 100755
index 0000000..bd61432
--- /dev/null
+++ b/scripts/clean-keydir
@@ -0,0 +1,146 @@
+#!/bin/bash
+
+# Copyright (c) 2012 Jonathan McDowell <noodles@earth.li>,
+# 2019 Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+# GNU GPL; v2 or later
+# Given a key directory, prune, clean, or minimize the keys
+
+# "prune" just does basic cleanup on the file, without getting rid of
+# any third-party signatures.
+
+set -e
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ cat >&2 <<EOF
+Usage: $0 [prune|launder|clean|minimal] dir
+ prune: remove invalid parts
+ launder: invoke GnuPG's merge logic to trim the key
+ clean: prune and drop non-debian third-party certifications
+ minimal: prune and remove *all* third-party certifications
+EOF
+ exit 1
+fi
+
+declare -a GPGOPTIONS=(--batch
+ --no-tty
+ --quiet
+ --no-options
+ --homedir=/dev/null
+ --trust-model=always
+ --fixed-list-mode
+ --with-colons
+ --export-options=no-export-attributes
+ )
+
+
+if [ "$1" == prune ]; then
+ GPGOPTIONS+=(--no-keyring
+ --import-options=import-export
+ )
+elif [ "$1" == launder ]; then
+ # we are going to do something very ugly...
+ # see https://dev.gnupg.org/T4421
+ : pass
+elif [ "$1" == clean ]; then
+ # we need to include all the known keys so that we keep the
+ # interlocking signatures
+ make
+ GPGOPTIONS+=(--no-default-keyring
+ --import-options=import-export,import-clean
+ --export-options=export-clean
+ --keyring "$(readlink -f output/keyrings/debian-keyring.gpg)"
+ --keyring "$(readlink -f output/keyrings/debian-nonupload.gpg)"
+ --keyring "$(readlink -f output/keyrings/debian-maintainers.gpg)"
+ --keyring "$(readlink -f output/keyrings/debian-role-keys.gpg)"
+ --keyring "$(readlink -f output/keyrings/emeritus-keyring.gpg)"
+ )
+elif [ "$1" == minimal ]; then
+ GPGOPTIONS+=(--no-keyring
+ --import-options=import-export,import-minimal
+ --export-options=export-minimal
+ )
+else
+ echo "Must specify prune, launder, clean or minimal; not $1" >&2
+ exit 1
+fi
+
+if [ ! -d "$2" ]; then
+ printf '%s is not a directory' "$2" >&2
+ exit 1
+fi
+
+# takes name of transferable public key file as $1, emits the laundered key to file named $2
+launder_tpk() {
+ local interim="$(mktemp -d interim.XXXXXXX)"
+ local success=false
+ local key="$1"
+ local output="$2"
+ mkdir -p -m 0700 "$interim/gpg" "$interim/split"
+ cat > "$interim/gpg/gpg.conf" <<EOF
+batch
+no-tty
+quiet
+no-options
+trust-model always
+fixed-list-mode
+with-colons
+export-options no-export-attributes
+EOF
+ if gpg --homedir "$interim/gpg" --import-options=import-minimal --status-file "$interim/status" --import < "$key" &&
+ fpr="$(awk '{ if ($1 == "[GNUPG:]" && $2 == "IMPORT_OK" && $3 == "1") { print $4 } }' < "$interim/status")" &&
+ [ -n "$fpr" ] &&
+ gpg --homedir "$interim/gpg" --export | (cd "$interim/split" && gpgsplit) &&
+ gpg --homedir "$interim/gpg" --delete-key "$fpr"; then
+ local pub="$interim/split/000001-006.public_key"
+ local uid=$(ls "$interim/split/"*.user_id | head -n1)
+ local sig=$(printf '%s/split/%06d-002.sig' "$interim" $(( "$(echo "${uid##$interim/split/}" | sed -e 's_^0*__' -e 's_-.*$__')" + 1 )) )
+ if [ -r "$pub" ] && [ -r "$uid" ] && [ -r "$sig" ]; then
+ if cat "$pub" "$uid" "$sig" | gpg --homedir "$interim/gpg" --import &&
+ gpg --homedir "$interim/gpg" --import < "$key" &&
+ gpg --homedir "$interim/gpg" --output "$output" --export "$fpr"; then
+ success=true
+ else
+ printf 'Merging failed for %s (fpr: %s)\n' "$key" "$fpr" >&2
+ fi
+ else
+ printf 'Could not find minimal TPK for %s (fpr: %s)\n' "$key" "$fpr" >&2
+ fi
+ else
+ printf 'failed to do initial import of %s\n' "$key" >&2
+ fi
+ rm -rf "$interim"
+ [ $success = true ]
+}
+
+cd "$2"
+for key in 0x*; do
+ success=false
+ if [ "$1" == launder ]; then
+ if launder_tpk "$key" "$key.new"; then
+ success=true
+ fi
+ else
+ if gpg "${GPGOPTIONS[@]}" --output "$key.new" --import "$key"; then
+ success=true
+ fi
+ fi
+ if [ $success = true ] && [ -s $key.new ]; then
+ OLDSIZE=$(stat -c "%s" "$key")
+ NEWSIZE=$(stat -c "%s" "$key.new")
+ if [ $OLDSIZE -gt $NEWSIZE ]; then
+ echo "Cleaning $key [$OLDSIZE] -> [$NEWSIZE]"
+ mv "$key.new" "$key"
+ elif [ $OLDSIZE -eq $NEWSIZE ] && ! cmp --quiet "$key" "$key.new" ; then
+ printf "Packets were reordered in $key"
+ if [ "$1" == launder ]; then
+ echo " (but ignoring while doing launder: https://dev.gnupg.org/T4422)"
+ else
+ mv "$key.new" "$key"
+ echo
+ fi
+ fi
+ fi
+ [ -e "$key.new" ] && rm "$key.new"
+done
+
+exit 0
diff --git a/scripts/explode-keyring b/scripts/explode-keyring
new file mode 100755
index 0000000..d6362ac
--- /dev/null
+++ b/scripts/explode-keyring
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+# Copyright (c) 2008 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+# Converts a keyring into individual keys.
+# Taken from jetring-explode, Copyright 2007 Joey Hess <joeyh@debian.org>
+
+set -e
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: explode-keyring keyring dir" >&2
+ exit 1
+fi
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+keyring=$(readlink -f "$1") # gpg works better with absolute keyring paths
+changesetdir="$2"
+
+basename=$(basename "$keyring")
+date=`date -R`
+
+mkdir -p "$changesetdir"
+
+for key in $(gpg --no-auto-check-trustdb --options /dev/null --no-default-keyring --keyring "$keyring" --list-keys --keyid-format long | grep '^pub' | sed -e 's!.*/!!' -e 's/ .*//'); do
+ out="$changesetdir/0x$key"
+ echo "$out"
+ gpg --no-auto-check-trustdb --options /dev/null \
+ --no-default-keyring --keyring "$keyring" \
+ --export-options no-export-attributes \
+ --export "$key" > "$out"
+done
diff --git a/scripts/gpg-diff b/scripts/gpg-diff
new file mode 100755
index 0000000..29f45ca
--- /dev/null
+++ b/scripts/gpg-diff
@@ -0,0 +1,204 @@
+#!/usr/bin/perl -w
+
+# Copyright (c) 2007 Anthony Towns
+# GNU GPL; v2 or later
+# Gives an overview of what changed between two keyrings
+
+# Take from jetring-diff and modified to be suitable for git.
+# Copyright (c) 2007 Jonathan McDowell <noodles@earth.li>
+
+use strict;
+use Cwd q{abs_path};
+use File::Temp qw(tempdir);
+use warnings;
+use strict;
+
+if (@ARGV != 2 and @ARGV != 7) {
+ die "usage: gpg-diff old.gpg new.gpg | path old.gpg old-hex old-mode ".
+ "new.gpg new-hex new-mode\n";
+}
+
+# avoid gnupg touching ~/.gnupg
+$ENV{GNUPGHOME}=tempdir("jetring.XXXXXXXXXX", TMPDIR => 1, CLEANUP => 1);
+
+my ($l, $r);
+
+if (@ARGV == 7) {
+ # Print a diff style header
+ print "gpg-diff a/$ARGV[0] b/$ARGV[4]\n";
+ print "--- a/$ARGV[0]\n";
+ print "+++ b/$ARGV[4]\n";
+ print "\n";
+
+ if ($ARGV[4] eq '/dev/null') {
+ print "Key deleted\n";
+ exit 0;
+ }
+
+ $l = parse_keyring($ARGV[1]);
+ $r = parse_keyring($ARGV[4]);
+} else {
+ $l = parse_keyring(shift);
+ $r = parse_keyring(shift);
+}
+
+foreach my $id (sort keys %{$l}) {
+ if (not exists $r->{$id}) {
+ summary("-", @{$l->{$id}});
+ }
+ else {
+ my $diff=0;
+ my @out;
+
+ my %rpackets = map { comparable($_->{'details'}) => $_ }
+ @{$r->{$id}};
+ my %lpackets = map { comparable($_->{'details'}) => 1 }
+ @{$l->{$id}};
+
+ foreach my $packet (@{$l->{$id}}) {
+ if (defined($rpackets{comparable($packet->{'details'})})) {
+ push @out, " ".outformat($packet->{'details'});
+ push @out, comparesigs(\$diff, $packet->{'sigs'},
+ $rpackets{comparable($packet->{'details'})}->{'sigs'});
+ } else {
+ push @out, "-".outformat($packet->{'details'});
+ $diff = 1;
+ }
+ }
+
+ foreach my $packet (@{$r->{$id}}) {
+ if (! $lpackets{comparable($packet->{'details'})}) {
+ push @out, "+".outformat($packet->{'details'});
+ $diff = 1;
+ }
+ }
+
+ print @out if $diff;
+ }
+}
+foreach my $id (sort keys %{$r}) {
+ if (not exists $l->{$id}) {
+ summary("+", @{$r->{$id}});
+ }
+}
+
+sub parse_keyring {
+ my $k=shift;
+
+ $k=abs_path($k); # annoying gpg..
+ my $cache=$k.".cache";
+
+ my $cached=0;
+ my $kmtime=(stat($k))[9];
+ if (-e $cache) {
+ my $cmtime=(stat($cache))[9];
+ if ($kmtime == $cmtime) {
+ open(DUMP, $cache) || die "$cache: $!";
+ $cached=1;
+ }
+ }
+ if (! $cached) {
+ open(DUMP, "gpg --options /dev/null --no-default-keyring ".
+ "--no-auto-check-trustdb --keyring $k --list-sigs ".
+ "--fixed-list-mode --with-colons -q |")
+ or die "couldn't dump keyring $k: $!";
+# Disable caching for the moment
+# if (! open(CACHE, ">$cache")) {
+# print STDERR "warning: cannot write cache $cache\n";
+ $cache=undef;
+# }
+ }
+ my %keys;
+ my $id;
+ my $packet;
+ while (<DUMP>) {
+ if (! $cached && defined $cache) {
+ print CACHE $_;
+ }
+ chomp;
+
+ my @fields=split(":", $_);
+ $fields[5]="-"; # ignore creation date, varies
+ next if $fields[0] eq 'tru';
+ if ($fields[0] eq 'pub') {
+ $id=$fields[4];
+ }
+ if ($fields[0] ne 'sig' && $fields[0] ne 'rev') {
+ if (defined($packet)) {
+ push @{$keys{$id}}, $packet;
+ undef $packet;
+ }
+ $packet->{'details'} = \@fields;
+ } else {
+ if (! defined $id or !defined($packet)) {
+ die "parse error: $_";
+ next;
+ }
+ push @{$packet->{'sigs'}}, \@fields;
+ }
+ }
+ push @{$keys{$id}}, $packet;
+ close DUMP;
+
+ if (defined $cache) {
+ close CACHE;
+ utime($kmtime, $kmtime, $cache) ||
+ print STDERR "warning: failed setting cache time: $!";
+ }
+
+ return \%keys;
+}
+
+sub summary {
+ my $prefix=shift;
+
+ foreach my $record (@_) {
+ if (ref $record eq 'HASH') {
+ summary($prefix, $record->{$_})
+ foreach reverse sort keys %$record;
+ }
+ else {
+ if ($record->[0] eq 'pub' || $record->[0] eq 'uid') {
+ print "$prefix".outformat($record);
+ }
+ }
+ }
+}
+
+sub outformat {
+ return join(":", @{shift()})."\n";
+}
+
+sub comparable {
+ my @record=@{shift()};
+ if ($record[0] eq 'sig') {
+ # Displayed user ids for sigs vary, so compare different
+ # ones the same. The user-id is what matters.
+ $record[9]="";
+ }
+ return join(":", @record);
+}
+
+sub comparesigs {
+ my $diff = shift;
+ my $l = shift;
+ my $r = shift;
+ my %lseen = map { comparable($_) => 1 } @{$l};
+ my %rseen = map { comparable($_) => 1 } @{$r};
+ my @out;
+
+ foreach my $record (@{$l}) {
+ if (! $rseen{comparable($record)}) {
+ push @out, "-".outformat($record);
+ ${$diff} = 1;
+ }
+ }
+ foreach my $record (@{$r}) {
+ if (! $lseen{comparable($record)}) {
+ push @out, "+".outformat($record);
+ ${$diff} = 1;
+ }
+ }
+
+ return @out;
+}
diff --git a/scripts/mail_expired.rb b/scripts/mail_expired.rb
new file mode 100755
index 0000000..f0124c7
--- /dev/null
+++ b/scripts/mail_expired.rb
@@ -0,0 +1,69 @@
+#!/usr/bin/ruby
+# coding: utf-8
+require 'yaml'
+
+ARGV[0] == '--send' or
+ raise RuntimeError, 'This will send many mails! Are you sure? Tell me to "--send"'
+
+%w(debian-keyring-gpg debian-maintainers-gpg debian-nonupload-gpg .git).each do |ck|
+ Dir.exists?(ck) or raise RuntimeError, 'Please run this script from the base keyring-maint git tree'
+end
+
+data = `make test`.split(/\n/).select {|lin| lin=~/expired on/}
+keys = {}
+expired_keys = {'keyring' => [],
+ 'maintainers' => [],
+ 'nonupload' => []}
+
+File.open('keyids','r') do |f|
+ f.readlines.each do |l|
+ l=~/^(0x[\dABCDEF]+) (.+) <(.+)>$/; keys[$1] = [$2,$3]
+ end
+end
+
+data.each do |l|
+ l=~/debian-(\w+).gpg:\s*(0x[\dABCDEF]+) expired on (.+) \(\)\s*$/
+ persondata = keys[$2] || ['NM (?)', nil]
+ expired_keys[$1] << {:key => $2, :name => persondata[0], :login => persondata[1], :date => $3}
+end
+
+proposed_exp = (Time.now + 2*365*86400).strftime '%a %d %b %Y %I:%M:%S %p %Z'
+expired_keys.each do |keyring, exp_k|
+ next if keyring == 'maintainers'
+ exp_k.each do |key|
+ IO.popen('mutt %s@debian.org -c keyring-maint@debian.org -s "Expired key in Debian -- %s (since %s)" -H -' %
+ [key[:login], key[:key], key[:date]], 'w') do |f|
+
+ f.puts 'From: Debian Keyring Maintainers <keyring-maint@debian.org>
+
+Hello %s <%s@debian.org>,
+
+According to our records, your key «%s», part of the Debian
+%s keyring, is expired since %s — Which means you will
+be, among other issues, not able to perform package uploads or vote in GRs!
+
+Please review your key\'s expiry date setting it to a sensible date in
+the future, and send it to our HKP server:
+
+ $ gpg --edit-key %s
+ (...)
+ gpg> expire
+ (...)
+ Key is valid for? (0) 2y
+ Key expires at %s
+ Is this correct? (y/N) y
+ (...)
+ gpg> save
+ $ gpg --keyserver keyring.debian.org --send-key %s
+
+And we will include it in our next keyring push (due towards the end
+of each month).
+
+Thanks,
+
+ - Gunnar Wolf
+ on behalf of the keyring maintenance team
+' % [ key[:name], key[:login], key[:key], keyring, key[:date], key[:key], proposed_exp, key[:key] ]
+ end
+ end
+end
diff --git a/scripts/move-key b/scripts/move-key
new file mode 100755
index 0000000..2c52c93
--- /dev/null
+++ b/scripts/move-key
@@ -0,0 +1,145 @@
+#!/bin/sh
+
+# Copyright (c) 2013 Gunnar Wolf <gwolf@debian.org>,
+# Based on 2008 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+# Moves an existing key to another keyring directory
+
+set -e
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: move-key keyid dir" >&2
+ exit 1
+fi
+
+key=$1
+destdir=$(readlink -f $2)
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+keyfp="<fixme>"
+if [ $(echo -n $key|wc -c) -eq 16 ]; then
+ key='0x'$(echo $key|tr a-z A-Z)
+elif [ $(echo -n $key|wc -c) -eq 40 ] ; then
+ keyfp=$key
+ key='0x'$(echo -n $key | cut -b 25-)
+fi
+
+if [ ! -d "$destdir" ] || echo "$destdir"|grep -q -- '-gpg/?$'; then
+ echo "Error: $destdir is not a valid keyring directory" >& 2
+ exit 1
+fi
+
+for dir in *-gpg/; do
+ if [ -f $dir/$key ]; then
+ keyfile=$(readlink -f "$dir/$key")
+ srcdir=$(readlink -f $dir)
+ break
+ fi
+done
+
+if [ "$srcdir" = "$destdir" ]; then
+ echo "Source and destination directories are the same: $srcdir" >& 2
+ exit 1
+fi
+
+if [ -z "$keyfile" ]; then
+ echo "Requested key '$key' not found"
+ exit 1
+fi
+
+keyuser=$(gpg --with-colons --keyid long --options /dev/null --no-auto-check-trustdb < $keyfile| grep '^pub' | cut -d : -f 10)
+
+echo ""
+echo "About to move key $key ($keyuser)"
+echo " FROM $srcdir"
+echo " TO $destdir"
+echo "Are you sure you want to update this key? (y/n)"
+read n
+
+if [ "x$n" = "xy" -o "x$n" = "xY" ]; then
+ add_to_keyid=""
+ echo -n "Enter full name of new key's owner: "
+ read name
+ echo -n 'RT issue ID this change closes, if any: '
+ read rtid
+
+ if ( echo $destdir | egrep -q 'debian-keyring-gpg/?$' ); then
+ log="Add new DD key $key ($name) (RT #$rtid)"
+ add_to_keyid=yes
+ dest=DD
+ action=add
+ elif ( echo $destdir | egrep -q 'debian-nonupload-gpg/?$' ); then
+ log="Add new nonuploading key $key ($name) (RT #$rtid)"
+ add_to_keyid=yes
+ dest=DN
+ action=add
+ elif ( echo $destdir | egrep -q 'debian-maintainer-gpg/?$' ); then
+ log="Add new DM key $key ($name) (RT #$rtid)"
+ dest=DM
+ action=add
+ elif ( echo $destdir | egrep -q 'emeritus-keyring-gpg/?$' ); then
+ log="Move $key to emeritus ($name) (RT #$rtid)"
+ action=remove
+ fi
+
+ git mv $keyfile $destdir
+ VERSION=$(head -1 debian/changelog | awk '{print $2}' | sed 's/[\(\)]//g')
+ RELEASE=$(head -1 debian/changelog | awk '{print $3}' | sed 's/;$//')
+ case $RELEASE in
+ UNRELEASED)
+ dch --multimaint-merge -D UNRELEASED -a "$log"
+ ;;
+ unstable)
+ NEWVER=$(date +%Y.%m.xx)
+ if [ "$VERSION" = "$NEWVER" ]
+ then
+ echo '* Warning: New version and previous released version are'
+ echo " the same: $VERSION. This should not be so!"
+ echo ' Check debian/changelog'
+ fi
+ dch -D UNRELEASED -v $NEWVER "$log"
+ ;;
+ *)
+ echo "Last release $VERSION for unknown distribution «$RELEASE»."
+ echo "Not calling dch, do it manually."
+ ;;
+ esac
+ git add debian/changelog
+
+ if [ ! -z "$add_to_keyid" ]; then
+ if oldkey=$(grep $key keyids); then
+ echo "Key already present in the keyids file:"
+ echo $oldkey
+ else
+ echo -n "Enter Debian login of new key: "
+ read login
+ echo "$key $name <$login>" >> keyids
+ sort keyids > keyids.$$ && mv keyids.$$ keyids
+ git add keyids
+ fi
+ fi
+
+ cat > git-commit-template <<EOF
+$log
+
+Action: $action
+Subject: $name
+Username: $login
+Role: $dest
+Key: $keyfp
+Key-type:
+RT-Ticket: $rtid
+Request-signed-by:
+Details:
+Notes: Move from <src> keyring
+EOF
+else
+ echo "Not moving key."
+fi
diff --git a/scripts/parse-email b/scripts/parse-email
new file mode 100755
index 0000000..bf457b9
--- /dev/null
+++ b/scripts/parse-email
@@ -0,0 +1,199 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+import datetime
+import email
+import os.path
+import re
+import subprocess
+import sys
+
+debug = False
+
+roles = {
+ 'DM': 'debian-maintainers-gpg',
+ 'DD': 'debian-keyring-gpg',
+ 'DN': 'debian-nonupload-gpg'
+}
+
+
+def do_dch(logmsg):
+ release = "unknown"
+ with open('debian/changelog', 'r') as f:
+ line = f.readline()
+ m = re.match("debian-keyring \((.*)\) (.*); urgency=", line)
+ version = m.group(1)
+ release = m.group(2)
+ if release == "UNRELEASED":
+ if debug:
+ print('dch --multimaint-merge -D UNRELEASED -a "' + logmsg + '"')
+ else:
+ subprocess.call(['dch', '--multimaint-merge', '-D', 'UNRELEASED',
+ '-a', logmsg])
+ elif release == "unstable":
+ newver = datetime.date.today().strftime("%Y.%m.xx")
+ if newver == version:
+ print(' * Warning: New version and previous released version are ')
+ print(' the same: ' + newver + '. This should not be so!')
+ print(' Check debian/changelog')
+ if debug:
+ print('dch -D UNRELEASED -v ' + newver + ' "' + logmsg + '"')
+ else:
+ subprocess.call(['dch', '-D', 'UNRELEASED', '-v', newver, logmsg])
+ else:
+ print("Unknown changelog release: " + release)
+
+ if not debug:
+ subprocess.call(['git', 'add', 'debian/changelog'])
+
+
+def do_git_template(logmsg, state):
+ with open('git-commit-template', 'w') as f:
+ f.write(logmsg)
+ f.write('\n\n')
+ f.write("Action: add\n")
+ f.write("Subject: " + state['name'] + "\n")
+ if 'username' in state:
+ f.write("Username: " + state['username'] + "\n")
+ f.write("Role: " + state['role'] + "\n")
+ f.write("Key: " + state['keyid'] + "\n")
+ f.write("Key-type: " + state['keytype'] + "\n")
+ f.write("RT-Ticket: " + state['rt'] + "\n")
+ f.write("Request-signed-by: \n")
+ f.write("Key-certified-by: \n")
+ if 'details' in state:
+ f.write("Details: " + state['details'] + "\n")
+ if state['role'] == 'DM' and 'agreement' in state:
+ f.write("Advocates:\n")
+ for a in state['advocates']:
+ f.write(" " + a + "\n")
+ f.write("Agreement: " + state['agreement'] + "\n")
+ f.write("BTS: " + state['bts'] + "\n")
+
+
+# Change to our basedir, assuming we're in <basedir>/scripts/<us>
+basedir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
+os.chdir(basedir)
+
+inadvocates = False
+state = {}
+state['advocates'] = []
+
+for line in sys.stdin:
+ line = line.rstrip()
+
+ if inadvocates:
+ if line[:9] == 'KeyCheck:':
+ inadvocates = False
+ else:
+ line = line.lstrip()
+ state['advocates'].append(line)
+ continue
+
+ m = re.match("X-RT-Ticket: rt.debian.org #(.*)$", line)
+ if m:
+ state['rt'] = m.group(1)
+ continue
+
+ m = re.match(" please add key ID (.*)$", line)
+ if m:
+ state['keyid'] = m.group(1)
+ state['role'] = 'DM'
+ state['move'] = False
+ continue
+
+ m = re.match("Please make (.*) \(currently ([^ ]*) ([^ ]*)", line)
+ if m:
+ state['name'] = m.group(1)
+ state['keytype'] = '<FIXME>'
+ state['move'] = (not ((m.group(2) == 'NOT') or (m.group(2) != "'Debian") or (m.group(3) == "Contributor')"))) or m.group(3) == 'DM)'
+ m = re.match(".*a non-uploading", line)
+ if m:
+ state['role'] = 'DN'
+ else:
+ state['role'] = 'DD'
+ continue
+
+ m = re.match("^Agreement: (.*)", line)
+ if m:
+ state['agreement'] = m.group(1)
+ continue
+
+ m = re.match("^BTS: (.*)$", line)
+ if m:
+ state['bts'] = m.group(1)
+ continue
+
+ m = re.match("^Comment: (Please add|Add) (.*) <", line)
+ if m:
+ state['name'] = m.group(2)
+ continue
+
+ m = re.match("^Advocates:(=20)?$", line)
+ if m:
+ inadvocates = True
+ continue
+
+ m = re.match(" pub (.....)/", line)
+ if m:
+ state['keytype'] = m.group(1)
+ continue
+
+ m = re.match(" Key fingerprint:\s+(.*)", line)
+ if m:
+ state['keyid'] = m.group(1)
+ continue
+
+ m = re.match(" Target keyring:\s+(Deb.*)", line)
+ if m:
+ if (m.group(1) == 'Debian Maintainer' or
+ m.group(1) == 'Debian Maintainer, with guest account'):
+ state['role'] = 'DM'
+ state['move'] = False
+ elif m.group(1) == 'Debian Developer, uploading':
+ state['role'] = 'DD'
+ elif m.group(1) == 'Debian Developer, non-uploading':
+ state['role'] = 'DN'
+ else:
+ state['role'] = "UNKNOWN"
+ continue
+
+ m = re.match(" (Account|Username):\s+(.*)", line)
+ if m:
+ state['username'] = m.group(2)
+ continue
+
+ m = re.match(" Details:\s+(http.*)", line)
+ if m:
+ state['details'] = m.group(1)
+ continue
+
+if 'role' not in state:
+ print('Did not find recognised keyring related email.')
+ sys.exit(1)
+
+if not debug:
+ if state['move']:
+ if os.path.exists(roles['DM'] + '/0x' + state['keyid'][24:]):
+ res = subprocess.call(["scripts/move-key", state['keyid'],
+ roles[state['role']]], stdin=open('/dev/tty'))
+ elif os.path.exists(roles['DN'] + '/0x' + state['keyid'][24:]):
+ res = subprocess.call(["scripts/move-key", state['keyid'],
+ roles[state['role']]], stdin=open('/dev/tty'))
+ else:
+ print('Trying to move non-existent key from DM keyring.')
+ sys.exit(1)
+ else:
+ res = subprocess.call(["scripts/add-key", state['keyid'],
+ roles[state['role']]], stdin=open('/dev/tty'))
+ if res:
+ print('Failed to add key.')
+ sys.exit(1)
+
+logmsg = ("Add new " + state['role'] + " key 0x" + state['keyid'][24:] + " (" +
+ state['name'] + ") (RT #" + state['rt'] + ")")
+
+#if not state['move']:
+# do_dch(logmsg)
+do_git_template(logmsg, state)
diff --git a/scripts/parse-git-changelog b/scripts/parse-git-changelog
new file mode 100755
index 0000000..d74e83b
--- /dev/null
+++ b/scripts/parse-git-changelog
@@ -0,0 +1,245 @@
+#!/usr/bin/python3
+
+from __future__ import print_function
+import email
+import re
+import sys
+
+def role_is_dd(role):
+ """
+ Check if a role is a DD role
+ """
+ return role.startswith("DD") or role.startswith("DN")
+
+class DakOutput(object):
+ """
+ Output parsed record for dak
+ """
+ def __init__(self, pathname):
+ self.out = open(pathname, 'w')
+ self.out.write("Archive: ftp.debian.org\n")
+ self.out.write("Uploader: Jonathan McDowell <noodles@earth.li>\n")
+ self.out.write("Cc: keyring-maint@debian.org\n")
+
+ def close(self):
+ self.out.close()
+
+ def write(self, state, operation):
+ if operation['action'] == 'remove':
+ if 'rt-ticket' in operation:
+ if not role_is_dd(operation['role']):
+ self.out.write("\nAction: dm-remove\n" +
+ "Fingerprint: " + operation['key'] + "\n" +
+ "Reason: RT #" + operation['rt-ticket'] +
+ ", keyring commit " + state['commit'] + "\n")
+ elif operation['action'] == 'replace':
+ if not role_is_dd(operation['role']):
+ self.out.write("\nAction: dm-migrate\n" +
+ "From: " + operation['old-key'] + "\n" +
+ "To: " + operation['new-key'] + "\n" +
+ "Reason: RT #" + operation['rt-ticket'] +
+ ", keyring commit " + state['commit'] + "\n")
+ elif (operation['action'] == 'add' and
+ role_is_dd(operation['role']) and
+ operation['notes'] == 'Move from DM keyring'):
+ if 'rt-ticket' in operation:
+ self.out.write("\nAction: dm-remove\n" +
+ "Fingerprint: " + operation['key'] + "\n" +
+ "Reason: Moved to DD keyring, RT #" +
+ operation['rt-ticket'] + ", keyring commit " +
+ state['commit'] + "\n")
+
+
+class RTOutput(object):
+ """
+ Output parsed records for RT
+ """
+ def __init__(self, pathname):
+ self.out = open(pathname, 'w')
+
+ def close(self):
+ self.out.close()
+
+ def write(self, state, operation):
+ if operation['action'] == 'add':
+ if 'rt-ticket' in operation:
+ self.out.write("# Commit " + state['commit'] + "\n")
+ if role_is_dd(operation['role']):
+ self.out.write("rt edit ticket/" + operation['rt-ticket'] +
+ " set queue=DSA\n")
+ elif operation['role'] == 'DM':
+ self.out.write("rt correspond -s resolved -m " +
+ "'This key has now been added to the active DM keyring.' " +
+ operation['rt-ticket'] + "\n")
+ else:
+ self.out.write("rt correspond -s resolved -m " +
+ "'This key has now been added to the " +
+ operation['role'] + " keyring.' " +
+ operation['rt-ticket'] + "\n")
+ elif operation['action'] == 'remove':
+ if 'rt-ticket' in operation:
+ self.out.write("# Commit " + state['commit'] + "\n")
+ if role_is_dd(operation['role']):
+ self.out.write("rt edit ticket/" + operation['rt-ticket'] +
+ " set queue=DSA\n")
+ else:
+ self.out.write("rt edit ticket/" + operation['rt-ticket'] +
+ " set queue=Keyring\n" +
+ "rt correspond -s resolved -m "+
+ "'This key has now been removed from the active DM keyring.' " +
+ operation['rt-ticket'] + "\n")
+ elif operation['action'] == 'replace':
+ self.out.write("# Commit " + state['commit'] + "\n")
+ if role_is_dd(operation['role']):
+ self.out.write("rt edit ticket/" + operation['rt-ticket'] +
+ " set queue=Keyring\n" +
+ "rt correspond -s resolved -m " +
+ "'Your key has been replaced in the active keyring and LDAP updated with the new fingerprint.' " +
+ operation['rt-ticket'] + "\n")
+ else:
+ self.out.write("rt edit ticket/" + operation['rt-ticket'] +
+ " set queue=Keyring\n" +
+ "rt correspond -s resolved -m "+
+ "'Your key has been replaced in the active DM keyring.' " +
+ operation['rt-ticket'] + "\n")
+
+
+class LDAPOutput(object):
+ """
+ Output parsed records for LDAP
+ """
+ def __init__(self, pathname):
+ self.out = open(pathname, 'w')
+
+ def close(self):
+ self.out.close()
+
+ def write(self, state, operation):
+ if operation['action'] == 'replace':
+ if role_is_dd(operation['role']):
+ self.out.write(operation['username'] + " " + operation['old-key'] + " ")
+ self.out.write(operation['new-key'] + "\n")
+
+
+class Parser(object):
+ def __init__(self):
+ self.seenrt = {}
+
+ def do_operation(self, state):
+ operation = email.message_from_string(state['message'])
+
+ if not 'action' in operation:
+ print("NOTE : " + state['commit'] + " (" + state['summary'] + ") has no action")
+ return None
+
+ if operation['role'] == 'role':
+ # At present we don't do anything with role keys
+ return None
+
+ if 'rt-ticket' in operation and operation['rt-ticket'] in self.seenrt:
+ print("ERROR: RT " + operation['rt-ticket'] + " used in " +
+ self.seenrt[operation['rt-ticket']] + " and " +
+ state['commit'])
+ else:
+ self.seenrt[operation['rt-ticket']] = state['commit']
+
+ if operation['action'] == 'add':
+ if 'rt-ticket' in operation:
+ if operation['role'] == 'DM':
+ try:
+ bts = operation['BTS'].strip()
+ bts = re.sub(r'https?://bugs.debian.org/(\d+)',
+ r'\1-done@bugs.debian.org', bts)
+ print("NOTE : Mail " + bts + " (new DM).")
+ except AttributeError:
+ print('NOTE : DM add for RT ticket %s lacks a BTS ticket.' % operation['RT-Ticket'])
+ return operation
+ else:
+ print("TODO : Add with no RT ticket")
+ return None
+ elif operation['action'] == 'remove':
+ if 'rt-ticket' in operation:
+ return operation
+ else:
+ if 'username' in operation:
+ username = operation['username']
+ elif 'key' in operation:
+ username = operation['key']
+ elif 'old-key' in operation:
+ username = operation['old-key']
+ elif 'subject' in operation:
+ username = operation['subject']
+ print("TODO : Removal for " + username + " without RT ticket.")
+ return None
+ elif operation['action'] == 'replace':
+ if role_is_dd(operation['role']):
+ if not 'username' in operation:
+ operation['Username'] = 'FIXME'
+ return operation
+ else:
+ return operation
+ else:
+ print("Error: Unknown action " + operation['action'])
+ return None
+
+ def main(self):
+ state = {}
+ opcount = 0
+ dak = DakOutput("dak-update")
+ rt = RTOutput("rt-update")
+ ldap = LDAPOutput("ldap-update")
+
+ for line in sys.stdin:
+ line = line.rstrip()
+
+ # Catch the start of a commit
+ m = re.match("commit (.*)$", line)
+ if m:
+ if 'message' in state:
+ operation = self.do_operation(state)
+ if operation:
+ dak.write(state, operation)
+ rt.write(state, operation)
+ ldap.write(state, operation)
+ opcount += 1
+ elif 'commit' in state:
+ if re.match("Import changes sent to keyring", state['summary']):
+ pass
+ elif re.match("Update changelog", state['summary']):
+ pass
+ else:
+ print("NOTE : " + state['commit'] + " (" + state['summary'] + ") is not an action.")
+ state = {}
+ state['commit'] = m.group(1)
+
+ if not re.match(" ", line):
+ continue
+
+ line = line[4:]
+ if not 'inaction' in state:
+ if not 'summary' in state:
+ state['summary'] = line
+ elif re.match("[a-zA-Z]*: ", line):
+ state['inaction'] = True
+ state['message'] = line + "\n"
+ else:
+ state['message'] += line + "\n"
+
+ # Process the last commit, if applicable
+ if 'message' in state:
+ operation = self.do_operation(state)
+ if operation:
+ dak.write(state, operation)
+ rt.write(state, operation)
+ ldap.write(state, operation)
+ opcount += 1
+
+ ldap.close()
+ rt.close()
+ dak.close()
+
+ print("Processed " + str(opcount) + " operations.")
+
+if __name__ == '__main__':
+ parser = Parser()
+ parser.main()
diff --git a/scripts/parse-gpg-update b/scripts/parse-gpg-update
new file mode 100755
index 0000000..844083e
--- /dev/null
+++ b/scripts/parse-gpg-update
@@ -0,0 +1,52 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use DB_File;
+
+my %ident;
+
+if ($#ARGV != 0 and $#ARGV != 1) {
+ print "Must supply key id.\n";
+ exit 1;
+}
+
+open KEYIDS, "<keyids" or die "Can't open keyids file: $!";
+while (<KEYIDS>) {
+ chomp;
+ /^0x([^ ]*) (.*)/;
+ $ident{$1} = $2;
+}
+close KEYIDS;
+
+$ARGV[0] =~ s/0x//;
+
+my $keyid = $ARGV[0];
+my $user;
+if (! defined($ident{$ARGV[0]})) {
+ if ($#ARGV == 1) {
+ $user = $ARGV[1] . " [DM]";
+ } else {
+ $user = "UNKNOWN (DM?)";
+ }
+} else {
+ $user = $ident{$ARGV[0]};
+}
+
+my ($uids, $subs, $sigs) = (0, 0, 0);
+while (<STDIN>) {
+ if (/new subkeys: (\d+)$/) {
+ $subs = $1;
+ } elsif (/new user IDs: (\d+)$/) {
+ $uids = $1;
+ } elsif (/new signatures: (\d+)$/) {
+ $sigs = $1;
+ }
+}
+
+print "0x$keyid $user";
+print " uid:$uids" if ($uids > 0);
+print " sub:$subs" if ($subs > 0);
+print " sig:$sigs" if ($sigs > 0);
+print "\n";
diff --git a/scripts/process-rt b/scripts/process-rt
new file mode 100755
index 0000000..87d5285
--- /dev/null
+++ b/scripts/process-rt
@@ -0,0 +1,667 @@
+#!/usr/bin/python3
+
+# Copyright (c) 2017-2018 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+#
+# Process RT tickets for keyring-maint@debian as raised by the
+# nm.debian.org web interface
+
+# Semi-helpful gpgme/Python examples:
+# https://pypkg.com/pypi/gpg/f/examples/
+
+import datetime
+import gpg
+import io
+import os
+import pprint
+import re
+import requests
+import subprocess
+import sys
+from urllib.parse import urlencode
+
+debug = False
+RT_BASE_URL = 'https://rt.debian.org/REST/1.0/'
+KEYSERVERS = ['keyserver.ubuntu.com:11371', 'the.earth.li:11371',
+ 'pool.sks-keyservers.net:11371' ]
+DAM = ['enrico', 'joerg', 'jmw']
+FD = DAM + ['noodles', 'mattia', 'peb', 'santiago', 'tobi', 'hartmans',
+ 'stuart', 'olasd']
+
+SIG_DAYS_WARN = 45
+DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
+
+# Try to find the keyring base directory, assuming we live in /scripts/
+basedir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0]))) + '/'
+if os.path.exists(basedir + 'debian-keyring-gpg'):
+ KEYRING_BASE_DIR = basedir
+else:
+ print("Can't find keyring directory.")
+ sys.exit(-1)
+GNUPG_HOME = KEYRING_BASE_DIR + 'gpghome/'
+
+# Maps roles to keyring directories
+role2keyring = {
+ 'DM': 'debian-maintainers-gpg',
+ 'DD': 'debian-keyring-gpg',
+ 'DN': 'debian-nonupload-gpg',
+ 'emeritus': 'emeritus-keyring-gpg',
+}
+
+# The keys must match nm2:backend/const.py:ALL_STATUS
+desc2role = {
+ 'Debian Developer, uploading': 'DD',
+ 'Debian Developer, non-uploading': 'DN',
+ 'Debian Maintainer': 'DM',
+ 'Debian Maintainer, with guest account': 'DM',
+ 'Debian Contributor': 'DC',
+ 'Debian Contributor, with guest account': 'DC',
+ 'Debian Developer, emeritus': 'emeritus',
+ 'Debian Developer, removed': 'removed',
+}
+
+fp_regex = ("[0-9A-F]{4} [0-9A-F]{4} [0-9A-F]{4} [0-9A-F]{4} [0-9A-F]{4} " +
+ "[0-9A-F]{4} [0-9A-F]{4} [0-9A-F]{4} [0-9A-F]{4} [0-9A-F]{4}")
+fp_regex_nospc = "[0-9A-F]{40}"
+
+# Global keyid to username/name dict
+keyids = {}
+
+
+def get_gpg_ctx(do_import=False):
+ """ Setup (if necessary) and return a GnuPG context
+
+ Checks if the private GnuPG home directory already exists. If not,
+ creates it and imports the DD + DN keyrings into it. Also configures
+ GnuPG to do clean imports (i.e. only include signatures that can be
+ verified).
+ """
+ if not os.path.isdir(GNUPG_HOME):
+ os.makedirs(GNUPG_HOME)
+ do_import = True
+
+ c = gpg.Context()
+
+ c.set_engine_info(gpg.constants.protocol.OpenPGP,
+ home_dir=GNUPG_HOME)
+
+ if do_import:
+ for keyring in ['debian-keyring', 'debian-nonupload']:
+ keyfile = KEYRING_BASE_DIR + 'output/keyrings/' + keyring + '.gpg'
+ if not os.path.exists(keyfile):
+ raise RuntimeError(keyfile + " does not exist. " +
+ "Need to run 'make'?")
+ keys = gpg.Data(file=keyfile)
+ c.op_import(keys)
+
+ with open(GNUPG_HOME + 'gpg.conf', 'w') as f:
+ f.write('import-options import-clean\n')
+
+ return c
+
+
+def fetch_key(ctx, fpr):
+ """Fetches the supplied fingerprint from a public keyserver
+
+ Does an HKP lookup for the supplied fingerprint, then imports it into
+ the current keyring. Then does an export (picking up any cleaning done
+ by the import) and returns the binary key data.
+
+ Note this function does not remove the key from the GnuPG keyring. If
+ the key is destined for the DM keyring, or subsequently not to be added,
+ it must be removed by the caller using delete_key().
+ """
+ gotkey = False
+ for keyserver in KEYSERVERS:
+ url = "http://{server}/pks/lookup?{query}".format(
+ server=keyserver,
+ query=urlencode({
+ "op": "get",
+ "search": "0x" + fpr,
+ "exact": "on",
+ }))
+ res = requests.get(url)
+ keytext = []
+ for line in res.text.splitlines():
+ if line == "-----BEGIN PGP PUBLIC KEY BLOCK-----":
+ gotkey = True
+ if gotkey:
+ keytext.append(line)
+ if line == "-----END PGP PUBLIC KEY BLOCK-----":
+ break
+ if gotkey:
+ break
+
+ if not gotkey:
+ raise RuntimeError('Failed to fetch key')
+
+ key = gpg.Data(string="\n".join(keytext))
+ ctx.op_import(key)
+
+ key = gpg.Data()
+ ctx.op_export(fpr, 0, key)
+ key.seek(0, os.SEEK_SET)
+ keydata = key.read()
+
+ return keydata
+
+
+def delete_key(ctx, fpr):
+ """Delete the key matching fpr from the GnuPG keyring"""
+ keys = list(ctx.keylist(fpr))
+ for k in keys:
+ ctx.op_delete(k, True)
+
+
+def get_keyinfo(ctx, fpr, needsigs=2):
+ ctx.set_keylist_mode(gpg.constants.keylist.mode.SIGS)
+ key = ctx.get_key(fpr)
+ for subkey in key.subkeys:
+ if subkey.fpr == fpr:
+ keytype = str(subkey.length)
+ if subkey.pubkey_algo == gpg.constants.pk.RSA:
+ keytype += 'R'
+ elif subkey.pubkey_algo == gpg.constants.pk.DSA:
+ keytype += 'D'
+ elif subkey.pubkey_algo == gpg.constants.pk.ECC:
+ keytype += 'E'
+ elif subkey.pubkey_algo == gpg.constants.pk.EDDSA:
+ keytype += 'E'
+ elif subkey.pubkey_algo == gpg.constants.pk.ELG:
+ keytype += 'g'
+ else:
+ keytype += '?'
+
+ sigs = {}
+ for uid in key.uids:
+ # print(uid.name, uid.email)
+ if not uid.revoked:
+ for sig in uid.signatures:
+ if sig.keyid in keyids:
+ sigs[keyids[sig.keyid]['username']] = 1
+ # else:
+ # print("Skipping unknown ID " + sig.keyid)
+
+ if len(sigs) < needsigs:
+ print('\nWARNING: Insufficient key signatures, check endorsements\n')
+
+ certs = None
+ for sig in sorted(sigs.keys()):
+ if certs:
+ certs += ', ' + sig
+ else:
+ certs = sig
+
+ return (keytype, certs)
+
+
+def read_keyids():
+ """Read the keyids file into a dict allowing a username/name mapping"""
+ with open(KEYRING_BASE_DIR + 'keyids', 'r') as f:
+ dds = f.readlines()
+ for dd in dds:
+ keyid = dd[2:18]
+ name = dd[19:dd.find('<') - 1]
+ username = dd[dd.find('<') + 1:]
+ username = username[:username.find('>')]
+ keyids[keyid] = {
+ 'name': name,
+ 'username': username,
+ }
+
+
+def write_keyids():
+ """Write the sorted keyids username/name dict out to the keyids file"""
+ with open(KEYRING_BASE_DIR + 'keyids', 'w') as f:
+ for key in sorted(keyids.keys()):
+ f.write("0x{} {} <{}>\n".format(key,
+ keyids[key]['name'],
+ keyids[key]['username']))
+
+
+def get_rt_auth():
+ """Attempt to locate a valid set of RT login details
+
+ Look for, and return, a set of RT login details. Uses RT_USER/RT_PASS
+ from the environment, failing back to ~/.rtrc if either is not set.
+ """
+ rt_user = None
+ rt_pass = None
+
+ if 'RT_USER' in os.environ:
+ rt_user = os.environ['RT_USER']
+ if 'RT_PASS' in os.environ:
+ rt_pass = os.environ['RT_PASS']
+
+ if not rt_user or not rt_pass:
+ with open(os.environ['HOME'] + '/.rtrc', 'r') as f:
+ for line in f:
+ if not rt_user and line.startswith('user '):
+ rt_user = line[5:].strip()
+ elif not rt_pass and line.startswith('passwd '):
+ rt_pass = line[7:].strip()
+
+ return (rt_user, rt_pass)
+
+
+def fetch_ticket(rtid):
+ (rt_user, rt_pass) = get_rt_auth()
+
+ args = {
+ 'params': {
+ 'user': rt_user,
+ 'pass': rt_pass,
+ }
+ }
+
+ print("Fetching ticket " + str(rtid))
+
+ # res = requests.post(RT_BASE_URL + 'ticket/' + str(rtid) + '/show',
+ # **args)
+ # Look for "Owner: Nobody" or "Owner: noodles"
+ # res = requests.post(RT_BASE_URL + 'ticket/' + str(rtid) + '/edit',
+ # **args)
+ # "content" variable = "Owner: noodles"
+
+ res = requests.post(RT_BASE_URL + 'ticket/' + str(rtid) + '/attachments',
+ **args)
+
+ # Validate the RT result
+ res_lines = res.text.splitlines()
+ ver, status, text = res_lines[0].split(None, 2)
+
+ if int(status) != 200:
+ print("RT status code is not 200", res_lines)
+ return
+
+ attachments = []
+ inattachments = False
+ text = None
+ signature = None
+
+ for line in res_lines[2:]:
+ if inattachments:
+ m = re.match(' +(\d+):', line)
+ if m:
+ print('Attachment found, ' + m.group(1))
+ attachments.append(int(m.group(1)))
+ else:
+ inattachments = False
+ else:
+ m = re.match('Attachments: (\d+):', line)
+ if m:
+ print('Attachment found, ' + m.group(1))
+ attachments.append(int(m.group(1)))
+ inattachments = True
+
+ for attachment in attachments:
+ res = requests.post(RT_BASE_URL + 'ticket/' + str(rtid) +
+ '/attachments/' + str(attachment),
+ **args)
+
+ # Validate the RT result
+ res_lines = res.text.splitlines()
+ ver, status, text = res_lines[0].split(None, 2)
+
+ if int(status) != 200:
+ print("RT status code is not 200", res_lines)
+
+ incontent = False
+ message = []
+ for line in res_lines[2:]:
+ if line.startswith('Content: '):
+ incontent = True
+ message.append(line[9:])
+ elif incontent and line.startswith(' '):
+ message.append(line[9:])
+ elif incontent:
+ incontent = False
+
+ with get_gpg_ctx() as c:
+ sig = None
+ try:
+ text, result = c.verify(io.BytesIO(
+ "\n".join(message).encode('utf-8')))
+ sig = result.signatures[0]
+ except gpg.errors.GPGMEError:
+ text = None
+ continue
+ except gpg.errors.BadSignatures as e:
+ # If a request is signed by multiple keys (such as old + new
+ # for a replacement) the important thing is one of the
+ # signatures is valid.
+ for s in e.results[1].signatures:
+ if s.status == 0:
+ text = e.results[0]
+ sig = s
+ break
+ else:
+ print("Bad signature from", s.fpr)
+ if sig:
+ key = c.get_key(sig.fpr)
+ # print(result.signatures[0].__str__())
+ for subkey in key.subkeys:
+ if subkey.fpr[24:] in keyids:
+ signature = keyids[subkey.fpr[24:]]['username']
+ print("Good signature from " + signature + " (" +
+ sig.fpr + ")")
+
+ sig_date = datetime.datetime.fromtimestamp(sig.timestamp)
+ now = datetime.datetime.now()
+ if (now - sig_date) >= datetime.timedelta(days=SIG_DAYS_WARN):
+ print((
+ "WARNING: Old GPG signature ({formatted_sig_date}, "
+ "today is {formatted_now}), please verify validity."
+ ).format(
+ formatted_sig_date=sig_date.strftime(DATE_FORMAT),
+ formatted_now=now.strftime(DATE_FORMAT),
+ ))
+ break
+ else:
+ print("Couldn't verify message.")
+
+ return (signature, text)
+
+
+def parse_ticket(text):
+ state = {}
+
+ for line in text.decode().split('\n'):
+ if line.startswith(' Key fingerprint: '):
+ if line[20:] != 'None':
+ state['keyid'] = line[20:]
+ elif line.startswith(' Username: '):
+ state['username'] = line[20:]
+ elif line.startswith(' uid: '):
+ state['username'] = line[20:]
+ elif line.startswith(' Details: '):
+ state['details'] = line[20:]
+ elif line.startswith(' First name: '):
+ state['first'] = line[20:]
+ elif line.startswith(' Middle name: '):
+ state['middle'] = line[20:]
+ elif line.startswith(' Last name: '):
+ state['last'] = line[20:]
+ elif line.startswith(' Current status: '):
+ if line[20:] not in desc2role:
+ print('Unknown current status: ' + line[20:])
+ else:
+ state['current'] = desc2role[line[20:]]
+ elif line.startswith(' Target keyring: '):
+ if line[20:] not in desc2role:
+ print('Unknown destination status: ' + line[20:])
+ else:
+ state['dest'] = desc2role[line[20:]]
+ elif 'details' not in state:
+ # Try to see if we have a fingerprint on the line and if
+ # it might be a replacement request
+ m = re.search(fp_regex, line)
+ if not m:
+ m = re.search(fp_regex_nospc, line)
+ if m:
+ fp = m.group(0).replace(' ', '')
+ if fp[24:] in keyids:
+ state['oldkeyid'] = fp
+ else:
+ state['keyid'] = fp
+
+
+ # Based on the current + target statuses work out if this is an add or
+ # remove.
+ if 'dest' not in state:
+ # Assume it's a replacement.
+ for role in role2keyring:
+ if os.path.exists(role2keyring[role] + '/0x' +
+ state['oldkeyid'][24:]):
+ state['role'] = role
+ break
+ if 'role' not in state:
+ state['role'] = 'DD'
+ state['action'] = 'replace'
+ state['subject'] = keyids[state['oldkeyid'][24:]]['name']
+ state['username'] = keyids[state['oldkeyid'][24:]]['username']
+ elif state['dest'] in ['DD', 'DM', 'DN']:
+ state['action'] = 'add'
+ state['role'] = state['dest']
+ elif state['dest'] in ['DC', 'emeritus', 'removed']:
+ state['action'] = 'remove'
+ if 'current' in state:
+ state['role'] = state['current']
+ else:
+ # Assume DD -> removed as a fall back
+ state['role'] = 'DD'
+
+ # Collapse first/middle/last to a single name field
+ if 'first' in state and state['first'] != '-':
+ state['subject'] = state['first']
+ if 'middle' in state and state['middle'] != '-':
+ if 'subject' in state:
+ state['subject'] += ' ' + state['middle']
+ else:
+ state['subject'] = state['middle']
+ if 'last' in state and state['last'] != '-':
+ if 'subject' in state:
+ state['subject'] += ' ' + state['last']
+ else:
+ state['subject'] = state['last']
+
+ for field in ['details', 'username']:
+ if field in state and state[field] == '-':
+ del state[field]
+
+ # Get the key length + type, plus signatures from other DDs
+ if 'keyid' in state:
+ with get_gpg_ctx() as c:
+ if state['action'] in ('add', 'replace') or state['role'] == 'DM':
+ state['keydata'] = fetch_key(c, state['keyid'])
+ # If it's a removal we don't need to check signature count. For
+ # DM we relax the number of signatures, otherwise we use the
+ # default.
+ if state['action'] == 'remove':
+ keyinfo = get_keyinfo(c, state['keyid'], 0)
+ elif state['role'] == 'DM':
+ keyinfo = get_keyinfo(c, state['keyid'], 1)
+ else:
+ keyinfo = get_keyinfo(c, state['keyid'])
+ state['keytype'] = keyinfo[0]
+ state['certs'] = keyinfo[1]
+
+ if 'oldkeyid' in state:
+ with get_gpg_ctx() as c:
+ fetch_key(c, state['oldkeyid'])
+ keyinfo = get_keyinfo(c, state['oldkeyid'], 0)
+ state['oldkeytype'] = keyinfo[0]
+
+ return state
+
+
+def do_action(state):
+ if state['action'] == 'remove':
+ if 'keyid' not in state:
+ # When inactive DDs with no valid keys are retired,
+ # there's nothing for us to do
+ print('-!- Nothing for keyring-maint to do, please reasign ticket')
+ print('-!- to DSA for account removal.')
+ sys.exit(-1)
+ if 'dest' in state and state['dest'] == 'emeritus':
+ subprocess.call(['git', 'mv', role2keyring[state['role']] + '/0x' +
+ state['keyid'][24:],
+ 'emeritus-keyring-gpg/'])
+ state['logmsg'] = ('Move 0x' + state['keyid'][24:] +
+ ' (' + state['subject'] + ') to ' +
+ 'emeritus (RT #' + state['rtid'] + ')')
+ else:
+ subprocess.call(['git', 'rm', role2keyring[state['role']] + '/0x' +
+ state['keyid'][24:]])
+ state['logmsg'] = ('Remove 0x' + state['keyid'][24:] +
+ ' (' + state['subject'] + ')' +
+ ' (RT #' + state['rtid'] + ')')
+ if state['role'] in ['DD', 'DN']:
+ with get_gpg_ctx() as c:
+ delete_key(c, state['keyid'])
+ elif state['action'] == 'add':
+ state['logmsg'] = ('Add new ' + state['dest'] + ' key 0x' +
+ state['keyid'][24:] + ' (' + state['subject'] +
+ ') (RT #' + state['rtid'] + ')')
+ # See if it's just a move from a different keyring
+ if state['current'] in role2keyring:
+ subprocess.call(['git', 'mv',
+ role2keyring[state['current']] + '/0x' +
+ state['keyid'][24:],
+ role2keyring[state['dest']]])
+ state['notes'] = 'Move from ' + state['current'] + ' keyring'
+ else:
+ keyfile = role2keyring[state['dest']] + '/0x' + state['keyid'][24:]
+ with open(keyfile, 'wb') as f:
+ f.write(state['keydata'])
+ subprocess.call(['git', 'add', keyfile])
+
+ # We don't keep DM keys in the our working keyring
+ if state['dest'] == 'DM':
+ with get_gpg_ctx() as c:
+ delete_key(c, state['keyid'])
+ keyids[state['keyid'][24:]] = {
+ 'name': state['subject'],
+ 'username': state['username'],
+ }
+ write_keyids()
+ subprocess.call(['git', 'add', 'keyids'])
+ elif state['action'] == 'replace':
+ state['logmsg'] = ('Replace 0x' + state['oldkeyid'][24:] + ' with 0x' +
+ state['keyid'][24:] + ' (' + state['subject'] +
+ ') (RT #' + state['rtid'] + ')')
+
+ keyfile = role2keyring[state['role']] + '/0x' + state['keyid'][24:]
+ with open(keyfile, 'wb') as f:
+ f.write(state['keydata'])
+ subprocess.call(['git', 'add', keyfile])
+ subprocess.call(['git', 'rm', role2keyring[state['role']] + '/0x' +
+ state['oldkeyid'][24:]])
+
+ # Remove the replaced key
+ with get_gpg_ctx() as c:
+ delete_key(c, state['oldkeyid'])
+
+ keyids[state['keyid'][24:]] = {
+ 'name': state['subject'],
+ 'username': state['username'],
+ }
+ write_keyids()
+ subprocess.call(['git', 'add', 'keyids'])
+ else:
+ print("Don't know how to handle action: " + state['action'])
+
+
+def do_dch(state):
+ release = "unknown"
+ with open('debian/changelog', 'r') as f:
+ line = f.readline()
+ m = re.match("debian-keyring \((.*)\) (.*); urgency=", line)
+ version = m.group(1)
+ release = m.group(2)
+ if release == "UNRELEASED":
+ if debug:
+ print('dch --multimaint-merge -D UNRELEASED -a "' +
+ state['logmsg'] + '"')
+ else:
+ subprocess.call(['dch', '--multimaint-merge', '-D', 'UNRELEASED',
+ '-a', state['logmsg']])
+ elif release == "unstable":
+ newver = datetime.date.today().strftime("%Y.%m.xx")
+ if newver == version:
+ print(' * Warning: New version and previous released version are ')
+ print(' the same: ' + newver + '. This should not be so!')
+ print(' Check debian/changelog')
+ if debug:
+ print('dch -D UNRELEASED -v ' + newver + ' "' + state['logmsg'] +
+ '"')
+ else:
+ subprocess.call(['dch', '-D', 'UNRELEASED', '-v', newver,
+ state['logmsg']])
+ else:
+ print("Unknown changelog release: " + release)
+
+ if not debug:
+ subprocess.call(['git', 'add', 'debian/changelog'])
+
+
+def do_git_template(state):
+ with open('git-commit-template', 'w') as f:
+ f.write(state['logmsg'])
+ f.write('\n\n')
+ f.write("Action: " + state['action'] + "\n")
+ f.write("Subject: " + state['subject'] + "\n")
+ if 'username' in state:
+ f.write("Username: " + state['username'] + "\n")
+ f.write("Role: " + state['role'] + "\n")
+ if state['action'] == 'replace':
+ f.write("Old-key: " + state['oldkeyid'] + "\n")
+ f.write("Old-key-type: " + state['oldkeytype'] + "\n")
+ f.write("New-key: " + state['keyid'] + "\n")
+ f.write("New-key-type: " + state['keytype'] + "\n")
+ else:
+ f.write("Key: " + state['keyid'] + "\n")
+ f.write("Key-type: " + state['keytype'] + "\n")
+ f.write("RT-Ticket: " + state['rtid'] + "\n")
+ f.write("Request-signed-by: " + state['requester'] + "\n")
+ if state['action'] != 'remove' and state['certs'] != None:
+ prefix = 'Key-certified-by: '
+ prefixlen = len(prefix)
+ certs = state['certs']
+ while (len(certs) + prefixlen) > 72:
+ last = certs.rfind(',', 0, 72 - prefixlen) + 1
+ f.write(prefix + certs[:last] + "\n")
+ certs = certs[last:]
+ prefix = ' '
+ prefixlen = 1
+ f.write(prefix + certs + "\n")
+ if 'details' in state:
+ f.write("Details: " + state['details'] + "\n")
+ if state['role'] == 'DM' and 'agreement' in state:
+ f.write("Advocates:\n")
+ for a in state['advocates']:
+ f.write(" " + a + "\n")
+ f.write("Agreement: " + state['agreement'] + "\n")
+ f.write("BTS: " + state['bts'] + "\n")
+ if 'notes' in state:
+ f.write('Notes: ' + state['notes'] + '\n')
+
+
+if len(sys.argv) < 2:
+ print('Must supply RT ticket to process.')
+ sys.exit(-1)
+
+# Change to the keyring dir so that git etc work
+os.chdir(KEYRING_BASE_DIR)
+
+read_keyids()
+(requester, ticket) = fetch_ticket(sys.argv[1])
+if ticket is None:
+ print('No signature on ticket.')
+ sys.exit(-1)
+if requester is None:
+ print('Signature from unknown key.')
+ sys.exit(-1)
+state = parse_ticket(ticket)
+state['rtid'] = sys.argv[1]
+state['requester'] = requester
+if state['action'] == 'add':
+ if requester not in FD:
+ print('Signature for add must come from Front Desk.')
+ sys.exit(-1)
+# Output our state for confirmation, without the keydata
+pp = pprint.PrettyPrinter()
+s = dict(state)
+if 'keydata' in s:
+ del s['keydata']
+pp.pprint(s)
+proceed = input("Do you wish to proceed? [y/n]: ")
+if proceed.lower() not in ['y', 'yes']:
+ print("Aborting.")
+ # If this was an add we need to make sure the key isn't in our keyring
+ if state['action'] in ['add', 'replace']:
+ delete_key(get_gpg_ctx(), state['keyid'])
+ sys.exit(-1)
+do_action(state)
+do_dch(state)
+do_git_template(state)
diff --git a/scripts/pull-updates b/scripts/pull-updates
new file mode 100755
index 0000000..1d49e08
--- /dev/null
+++ b/scripts/pull-updates
@@ -0,0 +1,90 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: pull-updates keyring [dir | keyring]" >&2
+ exit 1
+fi
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+cat > "$GNUPGHOME"/gpg.conf <<EOF
+keyid-format 0xlong
+import-options import-clean,merge-only
+export-options export-clean,no-export-attributes
+no-auto-check-trustdb
+no-autostart
+quiet
+EOF
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+if [ ! -e output/keyrings/debian-keyring.gpg ]; then
+ echo "Keyrings don't appear to be built. Run make?"
+ exit 1
+fi
+
+# Build a set of keyrings
+cat output/keyrings/debian-keyring.gpg output/keyrings/debian-nonupload.gpg \
+ output/keyrings/debian-maintainers.gpg > $GNUPGHOME/pubring.gpg
+
+mkdir updates/
+if [ ! -z "$2" -a -d "$2" ]; then
+ # Old style with directory as second parameter
+ scripts/explode-keyring $1 updates
+else
+ # New style. Keyrings all the way.
+ touch update-keyring.gpg
+ echo Exploding keyrings
+ for keyring in $*; do
+ scripts/explode-keyring $keyring updates
+ cd updates
+ for i in 0x*; do
+ if [ ! -e ../debian-*-gpg/$i ]; then
+ echo $i no longer exists, removing.
+ rm $i
+ elif cmp -s $i ../debian-*-gpg/$i; then
+ echo $i matches old key version, removing.
+ rm $i
+ fi
+ done
+ cat 0x* >> ../update-keyring.gpg
+ rm 0x*
+ cd ..
+ done
+ echo Importing updates
+ gpg --import update-keyring.gpg
+ echo Exploding keyring
+ for key in $(gpg --list-keys --with-colons < update-keyring.gpg | awk -F: '/^pub/ {print $5}'); do
+ gpg --export 0x$key > updates/0x$key
+ done
+ rm update-keyring.gpg
+fi
+
+cd updates
+for i in 0x*; do
+ if [ ! -e ../debian-*-gpg/$i ]; then
+ echo $i no longer exists, removing.
+ rm $i
+ elif cmp -s $i ../debian-*-gpg/$i; then
+ echo $i matches old key version, removing.
+ rm $i
+ fi
+done
+
+echo Updated keys are:
+ls
+
+cd ..
+
+for i in updates/0x*; do
+ if [ -f $i ]; then
+ scripts/update-key --no-clean $i \
+ $(dirname debian-*-gpg/$(basename $i))
+ rm $i
+ fi
+done
+
+rmdir updates/
diff --git a/scripts/replace-key b/scripts/replace-key
new file mode 100755
index 0000000..caad499
--- /dev/null
+++ b/scripts/replace-key
@@ -0,0 +1,178 @@
+#!/bin/bash
+
+# Copyright (c) 2014 Gunnar Wolf <gwolf@debian.org>,
+# Based on 2008 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+# Replaces an existing key with a new one in its same keyring directory
+
+set -e
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: replace-key oldkeyid newkeyid" >&2
+ exit 1
+fi
+
+scriptdir=`dirname $0`
+oldkey=$1
+newkey=$2
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+cat > "$GNUPGHOME"/gpg.conf <<EOF
+keyid-format 0xlong
+keyserver pgpkeys.eu
+no-autostart
+EOF
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+newkeytemp=`mktemp -t newkey.XXXXXXXXX`
+gpgconf --launch dirmngr
+gpg --recv-key "$newkey"
+gpg --no-auto-check-trustdb --options /dev/null \
+ --keyring output/keyrings/debian-keyring.gpg \
+ --keyring output/keyrings/debian-nonupload.gpg \
+ --keyring output/keyrings/debian-maintainers.gpg \
+ --export-options export-clean,no-export-attributes \
+ --export "$newkey" > $newkeytemp
+
+# strip leading 0x from fingerprints
+oldkey=${oldkey##0x}
+newkey=${newkey##0x}
+
+if [ $(echo -n $oldkey|wc -c) -eq 16 ]; then
+ key='0x'$(echo $oldkey|tr a-z A-Z)
+elif [ $(echo -n $oldkey|wc -c) -eq 40 ] ; then
+ key='0x'$(echo -n $oldkey | cut -b 25-)
+else
+ echo "Please supply either a long keyid or a full fingerprint for the old key."
+ exit 1
+fi
+
+for dir in *-gpg/; do
+ if [ -f $dir/$key ]; then
+ oldkeyfile=$(readlink -f "$dir/$key")
+ keydir=$(readlink -f $dir)
+ keyring=`basename $keydir`
+ break
+ fi
+done
+
+if [ -z "$oldkeyfile" -o -z "$keydir" ]; then
+ echo "Requested key '$oldkey' not found (looked for '*-gpg/$key')"
+ exit 1
+fi
+
+oldkeyfp=$(gpg --with-colons --fingerprint --no-auto-check-trustdb --no-default-keyring --keyring $oldkeyfile| grep '^fpr' | cut -d : -f 10)
+newkeyfp=$(gpg --with-colons --fingerprint --no-auto-check-trustdb --no-default-keyring --keyring $newkeytemp| grep '^fpr' | cut -d : -f 10)
+
+oldkeydata=$(gpg --with-colons --keyid long --options /dev/null --no-auto-check-trustdb < $oldkeyfile|grep '^pub')
+newkeydata=$(gpg --with-colons --keyid long --options /dev/null --no-auto-check-trustdb < $newkeytemp|grep '^pub')
+oldkeyuser=$(echo $oldkeydata | cut -d : -f 10)
+newkeyuser=$(echo $newkeydata | cut -d : -f 10)
+oldkeylen=$(echo $oldkeydata | cut -d : -f 3)
+newkeylen=$(echo $newkeydata | cut -d : -f 3)
+oldkeyalg=$(echo $oldkeydata | cut -d : -f 4)
+if [ "$oldkeyalg" == "1" ]; then
+ oldkeyalg='R'
+elif [ "$oldkeyalg" == "17" ]; then
+ oldkeyalg='D'
+elif [ "$oldkeyalg" == "22" ]; then
+ oldkeyalg='E'
+else
+ oldkeyalg='UNK'
+fi
+newkeyalg=$(echo $newkeydata | cut -d : -f 4)
+if [ "$newkeyalg" == "1" ]; then
+ newkeyalg='R'
+elif [ "$newkeyalg" == "17" ]; then
+ newkeyalg='D'
+elif [ "$oldkeyalg" == "22" ]; then
+ oldkeyalg='E'
+else
+ newkeyalg='UNK'
+fi
+echo $oldkeydata
+
+echo ""
+echo "About to replace key $oldkey ($oldkeyuser)"
+echo " with NEW key $newkey ($newkeyuser)"
+echo " in the $keyring keyring."
+echo "Are you sure you want to update this key? (y/n)"
+read n
+
+if [ "x$n" = "xy" -o "x$n" = "xY" ]; then
+ destkeyring="$keyring"
+ if ! $scriptdir/add-key $newkeytemp $destkeyring ; then
+ echo "add-key failed"
+ exit 1
+ fi
+
+ if [ "$keyring" = "debian-keyring-gpg" -o "$keyring" = "debian-nonupload-gpg" ]; then
+ name=`grep $newkey keyids | sed 's/^[^ ]* //'|sed s/\<.*//`
+ account=`grep $newkey keyids | sed 's/.*\<//'|sed s/\>$//`
+ if [ "$keyring" = "debian-nonupload-gpg" ]; then
+ role='DD-NU'
+ else
+ role='DD'
+ fi
+ elif [ "$keyring" = "debian-maintainers-gpg" ]; then
+ echo -n "Enter full name of new key: "
+ read name
+ role='DM'
+ else
+ echo "*** Key to be replaced is of a strange type (not DD, NonUplDD, DM)"
+ echo " Be sure you are doing the right thing before committing. Double-check"
+ echo " the log message, as it will most likely not be correct."
+ name="Unknown"
+ fi
+ echo -n 'RT issue ID this change closes, if any: '
+ read rtid
+ name=$(echo $name | sed -r 's/^ *(.*) *$/\1/')
+
+ log="Replace 0x$oldkey with 0x$newkey ($name) (RT #$rtid)"
+
+ git rm $oldkeyfile
+ VERSION=$(head -1 debian/changelog | awk '{print $2}' | sed 's/[\(\)]//g')
+ RELEASE=$(head -1 debian/changelog | awk '{print $3}' | sed 's/;$//')
+ case $RELEASE in
+ UNRELEASED)
+ dch --multimaint-merge -D UNRELEASED -a "$log"
+ ;;
+ unstable)
+ NEWVER=$(date +%Y.%m.xx)
+ if [ "$VERSION" = "$NEWVER" ]
+ then
+ echo '* Warning: New version and previous released version are'
+ echo " the same: $VERSION. This should not be so!"
+ echo ' Check debian/changelog'
+ fi
+ dch -D UNRELEASED -v $NEWVER "$log"
+ ;;
+ *)
+ echo "Last release $VERSION for unknown distribution «$RELEASE»."
+ echo "Not calling dch, do it manually."
+ ;;
+ esac
+ git add debian/changelog
+
+ cat > git-commit-template <<EOF
+$log
+
+Action: replace
+Subject: $name
+Username: $account
+Role: $role
+Old-key: $oldkeyfp
+Old-key-type: $oldkeylen$oldkeyalg
+New-key: $newkeyfp
+New-key-type: $newkeylen$newkeyalg
+RT-Ticket: $rtid
+Request-signed-by: \$oldkey
+New-key-certified-by: \$oldkey,
+EOF
+
+fi
diff --git a/scripts/revoke-key b/scripts/revoke-key
new file mode 100755
index 0000000..db0c37c
--- /dev/null
+++ b/scripts/revoke-key
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+# Copyright (c) 2008 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+# Imports a standalone revocation certificate
+
+set -e
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: revoke-key revocationcertfile dir" >&2
+ exit 1
+fi
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+revfile=$(readlink -f "$1") # gpg works better with absolute keyring paths
+keydir="$2"
+
+basename=$(basename "$revfile")
+date=`date -R`
+
+keyid=$(gpg --with-colons --keyid long --options /dev/null --no-auto-check-trustdb < $keyfile | grep '^pub' | cut -d : -f 5)
+
+if [ ! -e $keydir/0x$keyid ]; then
+ echo "0x$keyid isn't already in $keydir - new key or error."
+ exit 1
+fi
+
+gpg --import $keydir/0x$keyid
+gpg --import $revfile
+gpg --no-auto-check-trustdb --options /dev/null \
+ --export-options export-minimal,no-export-attributes \
+ --export $keyid > $GNUPGHOME/0x$keyid
+
+echo "Running gpg-diff:"
+scripts/gpg-diff $keydir/0x$keyid $GNUPGHOME/0x$keyid
+
+echo "Are you sure you want to update this key? (y/n)"
+read n
+
+if [ "x$n" = "xy" -o "x$n" = "xY" ]; then
+ mv $GNUPGHOME/0x$keyid $keydir/0x$keyid
+ echo "Updated key."
+else
+ echo "Not updating key."
+fi
diff --git a/scripts/update-key b/scripts/update-key
new file mode 100755
index 0000000..5ca2a9c
--- /dev/null
+++ b/scripts/update-key
@@ -0,0 +1,95 @@
+#!/bin/sh
+
+# Copyright (c) 2008 Jonathan McDowell <noodles@earth.li>
+# GNU GPL; v2 or later
+# Updates an existing key in a keyring dir
+
+set -e
+
+if [ "x$1" = "x--no-clean" ]; then
+ NOCLEAN=1
+ shift
+else
+ NOCLEAN=0
+fi
+
+
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: update-key keyfile dir" >&2
+ exit 1
+fi
+
+# avoid gnupg touching ~/.gnupg
+GNUPGHOME=$(mktemp -d -t jetring.XXXXXXXX)
+export GNUPGHOME
+cat > "$GNUPGHOME"/gpg.conf <<EOF
+keyid-format 0xlong
+no-auto-check-trustdb
+no-autostart
+EOF
+
+if [ "$NOCLEAN" = "0" ]; then
+ echo "import-options import-clean" >> "$GNUPGHOME"/gpg.conf
+ echo "export-options export-clean,no-export-attributes" >> "$GNUPGHOME"/gpg.conf
+ cat output/keyrings/debian-keyring.gpg \
+ output/keyrings/debian-nonupload.gpg \
+ output/keyrings/debian-maintainers.gpg > $GNUPGHOME/pubring.gpg
+fi
+
+trap cleanup exit
+cleanup () {
+ rm -rf "$GNUPGHOME"
+}
+
+keyfile=$(readlink -f "$1") # gpg works better with absolute keyring paths
+keydir="$2"
+
+basename=$(basename "$keyfile")
+date=`date -R`
+
+keyid=$(gpg --quiet --with-colons < $keyfile | grep '^pub' | head -n 1 | cut -d : -f 5)
+name=$(gpg --quiet --with-colons < $keyfile | grep '^uid' | head -n 1 | cut -d : -f 10 | sed -e 's/ <.*//')
+
+if [ ! -e $keydir/0x$keyid ]; then
+ echo "0x$keyid isn't already in $keydir - new key or error."
+ exit 1
+fi
+
+gpg --quiet --import $keydir/0x$keyid
+summary=$(gpg --import $keyfile 2>&1 | \
+ scripts/parse-gpg-update 0x$keyid "$name")
+gpg --export $keyid > $GNUPGHOME/0x$keyid
+
+case "$summary" in
+ *:*)
+ # Something changed
+ ;;
+ *)
+ # Nothing changed, exit
+ echo "No changes to $summary"
+ exit
+esac
+
+if cmp -s $GNUPGHOME/0x$keyid $keydir/0x$keyid; then
+ echo "No changes to 0x$keyid"
+ exit
+fi
+
+echo "Running gpg-diff:"
+(
+ echo $summary
+ echo
+ scripts/gpg-diff $keydir/0x$keyid $GNUPGHOME/0x$keyid
+) | sensible-pager
+
+echo "Are you sure you want to update this key? (y/n)"
+read n
+
+if [ "x$n" = "xy" -o "x$n" = "xY" ]; then
+ mv $GNUPGHOME/0x$keyid $keydir/0x$keyid
+ git add $keydir/0x$keyid
+ echo "Updated key."
+ echo $summary >> update.log
+else
+ echo "Not updating key."
+fi
diff --git a/scripts/update-keyrings b/scripts/update-keyrings
new file mode 100755
index 0000000..8e5ed6f
--- /dev/null
+++ b/scripts/update-keyrings
@@ -0,0 +1,405 @@
+#!/usr/bin/python3
+
+# Authors: Daniel Kahn Gillmor <dkg@fifthhorseman.net>,
+# Gunnar Wolf <gwolf@debian.org>,
+# Jonathan McDowell <noodles@earth.li>
+# License: Parts from dkg are GPLv3+
+
+import os
+from os import path
+import socket
+from subprocess import run, Popen, PIPE
+from shutil import chown, copy, copyfile, rmtree
+from distutils.dir_util import copy_tree
+import sys
+import tempfile
+import hashlib
+import codecs
+from multiprocessing.pool import ThreadPool
+from typing import List, Tuple, Optional
+import datetime
+from email.utils import parseaddr
+
+
+def check_environ(should_run_on: str = 'kaufmann.debian.org') -> None:
+ '''Make sure that we are running where we expect to run
+
+ The expectation is to run on kaufmann.debian.org, but this can be
+ bypassed for testing with the RUNANYWAY environment variable.
+ '''
+ if not (os.environ.get('RUNANYWAY', False) or
+ socket.getfqdn(socket.gethostname()) == should_run_on):
+ raise Exception('''
+This script is meant to be run in %s
+You can still run it if you are sure by setting
+$RUNANYWAY to a nonempty value.
+ ''' % (should_run_on))
+
+
+def wkd_localpart(incoming: bytes) -> str:
+ '''Z-base32 the localpart of an e-mail address
+
+ https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-08#section-3.1
+ describes why this is needed.
+
+ See https://tools.ietf.org/html/rfc6189#section-5.1.6 for a
+ description of the z-base32 scheme.
+ '''
+ zb32 = "ybndrfg8ejkmcpqxot1uwisza345h769"
+
+ b = hashlib.sha1(incoming).digest()
+ ret = ""
+ assert(len(b) * 8 == 160)
+ for i in range(0, 160, 5):
+ byte = i // 8
+ offset = i - byte * 8
+ # offset | bits remaining in k+1 | right-shift k+1
+ # 3 | 0 | x
+ # 4 | 1 | 7
+ # 5 | 2 | 6
+ # 6 | 3 | 5
+ # 7 | 4 | 4
+ if offset < 4:
+ n = (b[byte] >> (3 - offset))
+ else:
+ n = (b[byte] << (offset - 3)) + (b[byte + 1] >> (11 - offset))
+
+ ret += zb32[n & 0b11111]
+ return ret
+
+
+def getdomainlocalpart(line: bytes, domain: bytes) -> Optional[bytes]:
+ 'Get the localpart of the e-mail address of a GnuPG user ID line matching DOMAIN'
+ if line.startswith(b'uid:'):
+ uid = line.split(b':')[9]
+ uid = uid.decode()
+ _name, addr = parseaddr(uid) # if parsing fails, this returns ('', '')
+ localpart, _at, thisdomain = addr.rpartition('@')
+ if thisdomain == domain.decode():
+ return localpart.lower().encode()
+ return None
+
+
+def gpgbase(keyrings: List[str]) -> List[str]:
+ 'Return the standard set of options to invoke gpg in an automated way'
+ return ['gpg', '--batch', '--no-options', '--with-colons',
+ '--no-default-keyring',
+ '--homedir=/dev/null', '--trust-model=always',
+ '--fixed-list-mode'] + list(map(lambda k: '--keyring=' + k, keyrings))
+
+
+def emit_wkd_and_return_dane(localpart: bytes, domain: str, keyrings: List[str]) -> bytes:
+ '''For a given address, emit the WKD file, and return the DANE OPENPGKEY record
+
+ These are handled differently because we want to generate a
+ single, reproducible zonefile for the DNS, while we are generating
+ a tree of files for WKD.
+
+ The caller will assemble all of the OPENPGPKEY records into a
+ single zonefile.
+ '''
+ wkdstr = wkd_localpart(localpart)
+ # what do we do if this local part is not a proper encoding?
+ addr = codecs.decode(localpart) + '@' + domain
+ cmd = gpgbase(keyrings) + ['--output',
+ path.join('openpgpkey', domain, 'hu', wkdstr),
+ '--export-options',
+ 'export-clean',
+ '--export-filter',
+ 'keep-uid=mbox=' + addr,
+ '--export',
+ '<' + addr + '>']
+ run(cmd, check=True)
+ cmd = gpgbase(keyrings) + ['--export-options', 'export-dane,export-clean',
+ '--export-filter', 'keep-uid=mbox=' + addr,
+ '--export', '<' + addr + '>']
+ dane = run(cmd, stdout=PIPE, check=True)
+ return dane.stdout
+
+
+def build_wkd_and_dane(domain: str, keyrings: List[str]) -> None:
+ 'Publish WKD and DANE OPENPGPKEY for all domain-relevant OpenPGP certificates'
+ if not path.isdir('openpgpkey'):
+ os.mkdir('openpgpkey')
+ os.mkdir(path.join('openpgpkey', domain))
+ os.mkdir(path.join('openpgpkey', domain, 'hu'))
+
+ # FIXME: deal with IDN:
+ bytedomain = codecs.encode(domain)
+
+ lister = Popen(gpgbase(keyrings) +
+ ['--list-keys', '@' + domain], stdout=PIPE)
+
+ localparts = set(
+ map(lambda x: getdomainlocalpart(x, bytedomain), lister.stdout))
+ localparts.discard(None)
+
+ dane_map = {}
+
+ def runner(x: bytes) -> Tuple[bytes, bytes]:
+ return (x, emit_wkd_and_return_dane(x, domain, keyrings))
+
+ def add_to_zone(res: Tuple[bytes, bytes]) -> None:
+ dane_map[res[0]] = res[1]
+
+ pool = ThreadPool(None)
+ for localpart in localparts:
+ pool.apply_async(runner, (localpart,), {}, add_to_zone)
+
+ pool.close()
+ pool.join()
+ # make the policy file:
+ policyfile = open(path.join('openpgpkey', domain, 'policy'), 'wb')
+ del policyfile
+ # write out the zonefile all at once, ordered by the localpart
+ with open(path.join('_openpgpkey.' + domain + '.zone'), 'wb') as zonefile:
+ when = datetime.datetime.now()
+ # FIXME: inspect serial number from existing zonefile --
+ # update serial number if it was from the same day
+ serial = 0
+ zonefile.write(openpgpkey_zonefile_header(when, serial))
+ for local in sorted(dane_map.keys()):
+ zonefile.write(dane_map[local])
+
+
+def fix_perms(path: str) -> None:
+ '''Fix the permissions of a given directory
+
+ Ensures all files/directories are owned and writeable by the keyring group.
+ Additionally they must be readable by all and directories executable.
+ '''
+ try:
+ chown(path, group="keyring")
+ os.chmod(path, 0o775)
+ except:
+ pass
+ for root, dirs, files in os.walk(path):
+ for cur in dirs:
+ try:
+ chown(os.path.join(root, cur), group="keyring")
+ except:
+ pass
+ try:
+ os.chmod(os.path.join(root, cur), 0o775)
+ except:
+ pass
+ for cur in files:
+ try:
+ chown(os.path.join(root, cur), group="keyring")
+ except:
+ pass
+ try:
+ os.chmod(os.path.join(root, cur), 0o664)
+ except:
+ pass
+
+
+def publish(srcdir: str,
+ where: str = None) -> None:
+ '''Verify new keyrings in srcdir; if ok, then publish at where.
+
+ Verification ensures that the new keyrings are signed-off by a
+ member of debian's keyring-maint team.
+
+ Publication consists of verifying the keyrings, placing them where
+ onak can find them, produce a zonefile for OPENPGPKEY DANE
+ records, and a tree of files for WKD.
+
+ '''
+ if where is None:
+ prefix = os.environ.get('PREFIX', '/srv/keyring.debian.org')
+ else:
+ prefix = where
+ pendingdir = path.join(prefix, 'pending-updates')
+ hkpdir = path.join(prefix, 'keyrings-new')
+ outputdir = path.join(prefix, 'pub')
+ for direc in [srcdir, pendingdir, hkpdir, outputdir]:
+ if not path.isdir(direc):
+ raise Exception("%s is not a directory" % (direc))
+ srcdir = path.realpath(srcdir)
+ sha512fname = path.join(srcdir, 'sha512sums.txt')
+ if not path.exists(sha512fname):
+ raise Exception('sha512sums.txt not found in %s' % (srcdir))
+ placeholder = path.join(srcdir, 'keyrings', '.placeholder')
+ if path.exists(placeholder):
+ os.unlink(placeholder)
+ # gpgv needs the keyring in the filesystem, not just a file
+ # descriptor (https://dev.gnupg.org/T4608)
+ with tempfile.NamedTemporaryFile() as maint_keyring:
+ maint_keyring.write(keyring_maint_keys())
+ gpgvcall = [
+ 'gpgv',
+ '--enable-special-filenames',
+ '--keyring',
+ maint_keyring.name,
+ '--output',
+ '-',
+ sha512fname]
+ gpgvout = run(gpgvcall, stderr=PIPE, stdout=PIPE)
+ if gpgvout.returncode != 0:
+ raise Exception("gpg verification failed:\n%s" %
+ (codecs.decode(gpgvout.stderr)))
+ os.chdir(srcdir)
+ files_to_check = set(
+ path.join('keyrings', x + '.gpg') for x in [
+ 'debian-keyring',
+ 'debian-maintainers',
+ 'debian-nonupload',
+ 'debian-role-keys',
+ 'emeritus-keyring'])
+ unexpected_files = set()
+ for line in filter(lambda x: x, codecs.decode(gpgvout.stdout).split('\n')):
+ (indigest, fname) = line.split()
+ with open(fname, 'rb') as f:
+ data = f.read()
+ digest = hashlib.new('sha512', data=data).hexdigest()
+ if digest != indigest:
+ raise Exception(
+ 'mismatched digest for %s.\nWanted: %s\nGot: %s' %
+ (fname, indigest, digest))
+ if fname in files_to_check:
+ files_to_check.remove(fname)
+ else:
+ unexpected_files.add(fname)
+ if files_to_check:
+ raise Exception('No sha512 digest found for: %s' % (files_to_check))
+ if unexpected_files:
+ print(
+ 'unexpected files (maybe add them to files_to_check):',
+ unexpected_files)
+
+ keyrings = ['keyring', 'maintainers', 'nonupload']
+ for kname in keyrings:
+ kfile = path.join(pendingdir, 'debian-%s.gpg' % (kname))
+ if path.exists(kfile):
+ raise Exception(
+ 'Unhandled pending updates.\nKeyrings in %s should be dealt with and removed' %
+ (pendingdir))
+
+ for kname in keyrings:
+ kfile = path.join(hkpdir, 'debian-%s.gpg' % (kname))
+ copy(kfile, pendingdir)
+
+ print('Updating active keyrings.')
+ copy_tree(srcdir, outputdir)
+ fix_perms(outputdir)
+ print('Updating HKP keyrings.')
+ for kname in keyrings:
+ kfile = path.join(srcdir, 'keyrings', 'debian-%s.gpg' % (kname))
+ dst = os.path.join(hkpdir, os.path.basename(kfile))
+ copyfile(kfile, dst)
+ print('Publishing WKD and DANE data (may take a few minutes).')
+ with tempfile.TemporaryDirectory(prefix='pub_staging_', dir=prefix) as wkd_staging:
+ os.chdir(wkd_staging)
+
+ def dkeyring(name: str) -> str:
+ return path.join(srcdir, 'keyrings', 'debian-' + name + '.gpg')
+ build_wkd_and_dane('debian.org',
+ [dkeyring(x) for x in [
+ 'nonupload',
+ 'keyring',
+ 'role-keys']])
+ wkd_deploy_path = path.join(prefix, 'openpgpkey')
+ # not quite an atomic move:
+ if path.isdir(wkd_deploy_path):
+ os.rename(wkd_deploy_path, 'openpgpkey.old')
+ os.rename('openpgpkey', wkd_deploy_path)
+ fix_perms(wkd_deploy_path)
+ os.rename(
+ '_openpgpkey.debian.org.zone',
+ path.join(prefix, '_openpgpkey.debian.org.zone'))
+ fix_perms(path.join(prefix, '_openpgpkey.debian.org.zone'))
+ os.chdir(srcdir)
+ run(['static-update-component', 'openpgpkey.debian.org'], check=True)
+ run(['sudo', 'service', 'bind9', 'reload'], check=True)
+
+
+def openpgpkey_zonefile_header(timestamp: datetime.datetime = None, sequence: int = 0) -> bytes:
+ '''Return static DNS RRs for _openpgpkey.debian.org
+
+ These records were suggested by the Debian System Administration
+ (DSA) team.
+ '''
+ if timestamp is None:
+ timestamp = datetime.datetime.now()
+ return b'''; zonefile for OPENPGPKEY records (RFC 7929) for debian.org
+_openpgpkey.debian.org. 3600 IN SOA kaufmann.debian.org. hostmaster.debian.org. (
+ %d%02d ; serial
+ 1800 ; refresh (30 minutes)
+ 600 ; retry (10 minutes)
+ 1814400 ; expire (3 weeks)
+ 600 ; minimum (10 minutes)
+ )
+_openpgpkey.debian.org. 28800 IN NS sec2.rcode0.net.
+_openpgpkey.debian.org. 28800 IN NS nsp.dnsnode.net.
+_openpgpkey.debian.org. 28800 IN NS sec1.rcode0.net.
+
+''' % (int(timestamp.strftime('%Y%m%d')), sequence)
+
+
+def keyring_maint_keys() -> bytes:
+ '''Extract keyring-maint keys from the local system keyrings.
+
+On DSA-managed hosts, /srv/keyring.debian.org/keyrings is more recent
+and up-to-date so we prefer it. On other hosts that have the
+debian-keyring package installed, we can fall back to it.
+ '''
+ keyring_locations = [
+ '/srv/keyring.debian.org/keyrings',
+ '/usr/share/keyrings']
+ keyrings = ['debian-keyring.gpg', 'debian-nonupload.gpg']
+ keyring_maint_uids = ['Jonathan McDowell <noodles@earth.li>',
+ 'William John Sullivan <johns@debian.org>',
+ 'Gunnar Wolf <gwolf@debian.org>',
+ 'Daniel Kahn Gillmor <dkg@debian.org>']
+ keyring_files = None
+ for loc in keyring_locations:
+ possible_keyrings = [path.join(loc, k) for k in keyrings]
+ if path.isdir(loc) and all(
+ map(lambda k: path.exists(k), possible_keyrings)):
+ keyring_files = possible_keyrings
+ break
+
+ if keyring_files is None:
+ raise Exception(
+ "Could not find keyrings to extract keyring-maint keys")
+
+ gpgcmd = ['gpg',
+ '--batch',
+ '--homedir',
+ '/dev/null',
+ '--no-options',
+ '--no-default-keyring',
+ '--export-options',
+ 'export-minimal']
+ for k in keyring_files:
+ gpgcmd += ['--keyring', k]
+ gpgcmd += ['--export']
+ gpgcmd += ['=' + u for u in keyring_maint_uids]
+
+ return run(gpgcmd, stdout=PIPE, check=True).stdout
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ raise Exception('Must provide directory containing new keyrings.')
+ elif len(sys.argv) > 2:
+ sys.argv.pop(0)
+ subcommand = sys.argv.pop(0)
+ if subcommand != 'build-wkd':
+ raise Exception("do not know this subcommand: %s" % (subcommand))
+ if len(sys.argv):
+ domain = sys.argv.pop(0)
+ else:
+ domain = 'debian.org'
+ if len(sys.argv):
+ keys = sys.argv
+ else:
+ keys = ['/usr/share/keyrings/debian-nonupload.gpg',
+ '/usr/share/keyrings/debian-keyring.gpg',
+ '/usr/share/keyrings/debian-role-keys.gpg']
+ build_wkd_and_dane(domain, keys)
+ else:
+ # standard update-keyrings
+ check_environ()
+ publish(sys.argv[1])
diff --git a/scripts/update-ldap b/scripts/update-ldap
new file mode 100755
index 0000000..02b9b0f
--- /dev/null
+++ b/scripts/update-ldap
@@ -0,0 +1,82 @@
+#!/usr/bin/python3
+
+# Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+# Date: 2014-08-30
+# License: GPLv3+
+
+# For doing keyring-maint tasks with debian LDAP
+
+import ldap
+import getpass
+import sys
+
+if sys.argv.__len__() != 2 or sys.argv[1] in ['-h', '--help', 'help']:
+ print('''Usage: update-ldap <FILENAME>
+
+FILENAME should be a simple text file, utf8 encoded, where each line
+is a key replacement with three fields separated by a single space:
+
+uid oldfpr newfpr
+''')
+ exit(1)
+
+
+
+class debldap:
+ def __init__(self):
+ self.l = ldap.initialize("ldaps://db.debian.org")
+
+ def auth(self, uid, password):
+ self.l.simple_bind_s("uid={uid},ou=users,dc=debian,dc=org".format(uid=uid),password)
+
+ def changefpr(self, uid, oldfpr, newfpr):
+ dn = "uid={uid},ou=users,dc=debian,dc=org".format(uid=uid)
+ objs = self.l.search_s(dn, ldap.SCOPE_SUBTREE, "objectclass=*")
+ if not objs:
+ raise BaseException("No objects found matching {dn}".format(dn=dn))
+ for o in objs:
+ if o[0] != dn:
+ raise BaseException("Weird/unexpected dn {new} (expected {old})".format(new=o[0], old=dn))
+ fprs = o[1]['keyFingerPrint']
+ if fprs != [oldfpr.encode('ascii')]:
+ raise BaseException("old fingerprint was {found}, but we expected {oldfpr}".format(found=fprs, oldfpr=oldfpr))
+ self.l.modify_s(dn, [(ldap.MOD_REPLACE, 'keyFingerPrint', [newfpr.encode('ascii')])])
+
+
+
+f = open(sys.argv[1])
+
+x = debldap()
+username = getpass.getuser()
+
+try:
+ passwd = getpass.getpass('Debian LDAP password for {user}: '.format(user=username))
+ x.auth(username, passwd)
+ bound = True
+except BaseException as e:
+ print("Failed to authenticate: {m}".format(m=e.message))
+ exit(1)
+
+errors = []
+lineno = 0
+successes = 0
+for line in f:
+ lineno += 1
+ user = '<unknown>'
+ try:
+ data = line.strip('\n').split(' ')
+ if data.__len__() != 3:
+ raise BaseException("ignoring malformed line: {line}\n".format(lineno=lineno, line=line))
+ user = data[0]
+ x.changefpr(user, data[1], data[2])
+ successes += 1
+ except BaseException as e:
+ print("{lineno}: {user}: {message}".format(lineno=lineno, user=user,message=str(e.message).strip()))
+ errors.append((lineno, e))
+
+
+print("{errors} errors, {successes} successfully processed".format(successes=successes, errors=errors.__len__()))
+
+if errors:
+ exit(1)
+