├── README.org ├── genphrase.py ├── genkeyid.py ├── genid.sh ├── keytemplate ├── mails.sh ├── README.genkeyid.org ├── setfp.py └── genkey /README.org: -------------------------------------------------------------------------------- 1 | see http://www.ctrlc.hu/~stef/blog/posts/PGP_key_generation.html 2 | -------------------------------------------------------------------------------- /genphrase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # (c) 2012 s@ctrlc.hu 3 | # 4 | # This is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | from Crypto import Random 10 | import struct, sys 11 | 12 | wf=open('words','r') 13 | words=wf.readlines() 14 | wf.close() 15 | 16 | i=0 17 | res=[] 18 | while ilen(words): 21 | idx=struct.unpack('!I', '\0'+Random.get_random_bytes(3))[0] 22 | res.append(words[idx].strip()) 23 | i+=1 24 | print ' '.join(res) 25 | -------------------------------------------------------------------------------- /genkeyid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # invoke with 4 | # python ./genkeyid.py keyring.pub 2>gpk.snapshots | grep --color -i -f keycandidates | tee fps 5 | 6 | import hashlib, struct, time, datetime, sys 7 | from pgpdump.packet import old_tag_length 8 | 9 | inkey=open(sys.argv[1],'rb') 10 | pubkey=inkey.read() 11 | inkey.close() 12 | 13 | now=time.time() 14 | 15 | try: 16 | # resume from last snapshot 17 | i=int(sys.argv[2]) 18 | except: 19 | i=0 20 | 21 | # patch date 22 | offset, length = old_tag_length(bytearray(pubkey),0) 23 | header=''.join(['\x99', 24 | struct.pack('!H', length), 25 | pubkey[offset+1:offset+2]]) 26 | trailer=pubkey[offset+6:offset+1+length] 27 | while i>sys.stderr, m.hexdigest(), i, datetime.datetime.fromtimestamp(i) 36 | now=time.time() 37 | 38 | -------------------------------------------------------------------------------- /genid.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ksh -x 2 | 3 | KeyType=RSA 4 | masterkeysize=1024 5 | username=joe 6 | usercmt="test" 7 | email="j@example.com" 8 | masterexpire=0 9 | 10 | function createkey { 11 | gpg -q --gen-key --batch <( 12 | cat <$1/snapshots | grep --line-buffered --color -i "$2" | tee $1/fps | read line 42 | [[ -n "$line" ]] && break 43 | done 44 | 45 | [[ -z "$line" ]] && exit 1 46 | echo $line 47 | python ./setfp.py $secring $pubring $(echo "$line" | cut -d' ' -f2) 48 | -------------------------------------------------------------------------------- /keytemplate: -------------------------------------------------------------------------------- 1 | usercmt="master key" # your nickname 2 | username="John Doe" # your fullname 3 | email="jd@example.com" # the comment field from PGP 4 | 5 | KeyType="RSA" # DSA is too small :/ 6 | 7 | #masterkeysize="8192" # enlarge your keysize! 8 | masterkeysize="1024" # enlarge your keysize! 9 | masterphraselen="5" # 5 words is a minimum 10 | masterexpire="0" # Master key should never expire 11 | masterpass="" # autogenerate 12 | 13 | #signkeysize="4096" 14 | signkeysize="1024" 15 | signphraselen="5" # 5 words is a minimum 16 | signexpire="1y" # 1 year 17 | signpass="" # autogenerate 18 | 19 | #cryptkeysize="4096" 20 | cryptkeysize="1024" 21 | cryptphraselen="5" # 5 words is a minimum 22 | cryptexpire="1y" # 1 year 23 | cryptpass="" # autogenerate 24 | 25 | backupshares="5" # generate 5 shares 26 | backupsharesneeded="4" # of which 4 are necessary to reconstruct the key 27 | 28 | revcertshares="5" # also the revocation cert should be split into 5 shares 29 | revcertsharesneeded="4" # 4 shares needed to reconstruct the cert 30 | -------------------------------------------------------------------------------- /mails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # reads e-mail addresses (one per row) from `mails.txt` and writes 4 | # e-mail bodies into files inside the `mails` directory, putting the 5 | # e-mail address in the filename, with a `.txt` extension -- these 6 | # can be changed below 7 | # expects your name in $1 - e.g. "jd (John Doe)" 8 | # and your fingerprint in $2 - e.g. "0000 0000 0000 0000 0000 0000 0000 0000" 9 | 10 | # author: András Veres-Szentkirályi , s@ctrlc.hu 11 | # source code is licensed under MIT, textual content under CC-BY-SA 12 | 13 | INPUT=mails.txt 14 | OUTDIR=mails 15 | mkdir -p $OUTDIR 16 | 17 | while read i; do 18 | cat >$OUTDIR/$i.txt <<-MAIL 19 | Hi, 20 | I'm $1 and we both participated in a 21 | keysigning party in the last few months. To sign your UID, I need 22 | to be sure that you control the e-mail address in the UID and the 23 | secret key you signed it with. To prove this, please reply to this 24 | e-mail in a way that it contains this original text, along with 25 | the request identifier string below. Please encrypt the reply with 26 | my public key ($2). 27 | 28 | Signing it with your key is optional, since you already proved you 29 | possess the private key by decrypting this e-mail. 30 | 31 | Request ID: $(uuidgen) 32 | 33 | Upon successful verification, I will sign your UID(s), and if you 34 | allowed/requested it, I will upload the signature to the keyserver. 35 | 36 | If you have any questions or would like me to perform a similar, 37 | verification, feel free to include these in your reply. 38 | -- 39 | Regards, 40 | $1 41 | MAIL 42 | done < $INPUT 43 | -------------------------------------------------------------------------------- /README.genkeyid.org: -------------------------------------------------------------------------------- 1 | * genkeyid 2 | 3 | A tool that helps you bruteforce arbitrary PGP keyids by modifying the 4 | timestamp field so that the packet hashes to a given keyid. 5 | 6 | Since this is a bruteforce attack on the hash algorithm to produce a 7 | certain pattern in the hash we can increase our chances when we look 8 | for multiple patterns. If the goal is not one specific keyid, but the 9 | goal is to generate one of many keyids, then you can prepare a list of 10 | multiple variations that would be acceptable, which increases your 11 | chances considerably to find a good keyid with a plausible date. 12 | 13 | * genkeyid.py 14 | 15 | genkeyid.py takes a file containing a single public key and starts to 16 | emit all possible keyids by incrementing the Public Key Creation Time 17 | 32bit integer from 0 upwards, recalculating the resulting hash. You 18 | then have to filter this list of keyids, is it is a lot of data. 19 | 20 | A basic invocation of genkeyid would look like this: 21 | 22 | #+BEGIN_SRC sh 23 | python ./genkeyid.py keyring.pub 2>snapshots | grep --color -i -f keycandidates | tee fps 24 | #+END_SRC 25 | 26 | Where keyring.pub is either a keyring with only one public key, or 27 | your exported public key. The regular expressions per line in 28 | keycandidates serve as filters (see example later), as genkeyid prints 29 | out all possible keyids, and that is rather a lot of data, so you only 30 | store your "hits" in the file "fps". 31 | 32 | genkeyid emits snapshot ids to standard error, so you can always 33 | resume it from some intermediate position by supplying the 32 bit 34 | integer as the second parameter after the public key in genkeyid.py 35 | runs to resume or skip processing (see an example in genid.sh). 36 | 37 | * setfp.py 38 | When this exhaustive bruteforce is finished after a few hours on a 3 39 | year old laptop, you will have a list with potential keyids with 40 | various dates that complement the hash to the specific keyid. You 41 | choose the integer from row two and use setfp.py to patch your key 42 | like this: 43 | 44 | #+BEGIN_SRC sh 45 | python ./setfp.py " 46 | #+END_SRC 47 | 48 | Be aware that patching the keys only works with unencrypted private 49 | keys, so you should probably do this 2nd patching step only on a clean 50 | offline system, that you afterwards shred, if you care about good key 51 | hygene. 52 | 53 | * genid.sh 54 | There's also genid.sh which tries to automatically generate a new key 55 | matching a given keyid. There's a few optimisations that burn much 56 | more entropy but generate quite recent keys: 57 | 58 | #+BEGIN_SRC sh 59 | ksh -x ./genid.sh test "^DEADBEEF " 60 | #+END_SRC 61 | 62 | notice the format of the regex, it is anchored at the start of the 63 | line, and depends on matching up to the first space, as the format 64 | that genkeyid produces and which is grepped looks like this: 65 | 66 | #+BEGIN_SRC sh 67 | 690f3700 1357248344 50 e5 f7 58 2013-01-03 22:25:44 68 | #+END_SRC 69 | 70 | Of course you could as mentioned earlier also look for more keyids 71 | with more complex regular expressions. 72 | 73 | So when you're done, you could do (presuming you have the binary 74 | pgpdump installed): 75 | 76 | #+BEGIN_EXAMPLE 77 | diff -urw <(pgpdump test/pubring.pgp) <(pgpdump test/pubring.pgp-new) 78 | --- /proc/self/fd/11 2013-04-04 00:36:23.307190845 +0200 79 | +++ /proc/self/fd/12 2013-04-04 00:36:23.307190845 +0200 80 | @@ -1,6 +1,6 @@ 81 | Old: Public Key Packet(tag 6)(141 bytes) 82 | Ver 4 - new 83 | - Public key creation time - Thu Apr 4 00:32:22 CEST 2013 84 | + Public key creation time - Fri Jan 4 15:06:30 CET 2013 85 | Pub alg - RSA Encrypt or Sign(pub 1) 86 | RSA n(1024 bits) - ... 87 | RSA e(17 bits) - ... 88 | @@ -36,7 +36,7 @@ 89 | Hashed Sub: key server preferences(sub 23)(1 bytes) 90 | Flag - No-modify 91 | Sub: issuer key ID(sub 16)(8 bytes) 92 | - Key ID - 0xD39531EF87CB6015 93 | - Hash left 2 bytes - a2 5b 94 | - RSA m^d mod n(1022 bits) - ... 95 | + Key ID - 0xEDB069BE93FB0000 96 | + Hash left 2 bytes - 7c b8 97 | + RSA m^d mod n(1024 bits) - ... 98 | -> PKCS-1 99 | #+END_EXAMPLE 100 | -------------------------------------------------------------------------------- /setfp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys, struct, hashlib, traceback, datetime 4 | from pgpdump import utils 5 | from pgpdump.packet import old_tag_length 6 | from Crypto.PublicKey import RSA 7 | from Crypto import Random 8 | 9 | def help(): 10 | print "usage: $0 " 11 | sys.exit(1) 12 | 13 | def patchkey(key, i, rsakey, fp): 14 | # find signature packet 15 | offset=0 16 | # must be secret/public key packet type 17 | if ((key[0] & 0x3f) >> 2) not in [5, 6]: 18 | print "data does not start with a key packet" 19 | sys.exit(1) 20 | o2, l = old_tag_length(key,offset) 21 | datestart=offset+2+o2 22 | offset+=1 + o2 + l 23 | # next must be keyid packet type 24 | if ((key[offset] & 0x3f) >> 2) !=13: 25 | print "packet is not a keyid" 26 | sys.exit(1) 27 | # nothing to see here - skip to next 28 | o2, l = old_tag_length(key,offset) 29 | offset+=1 + o2 + l 30 | # next packet must be signature packet type 31 | if ((key[offset] & 0x3f) >> 2)!=2: 32 | print "packet is not a signature" 33 | sys.exit(1) 34 | # skip to end of hashed data 35 | o2, l = old_tag_length(key,offset) 36 | offset+=struct.unpack('!H', str(key[offset+5+o2:offset+7+o2]))[0]+7+o2 37 | #print "end of hashed data:", offset 38 | #print ' '.join(["%02x" % x for x in key[offset:offset+32]]) 39 | if not key[offset+2:offset+4]==bytearray([9,0x10]): 40 | print "issuer not found" 41 | sys.exit(1) 42 | 43 | # calculate hash of data to be signed 44 | hash=hashlib.sha1(str(key[:offset])).digest() 45 | 46 | # find out offset to store the keys 47 | hstart=offset+struct.unpack('!H', str(key[offset:offset+2]))[0]+2 48 | 49 | # patch date 50 | key[datestart:datestart+4]=struct.pack('!i',i) 51 | 52 | # patch issuer id 53 | key[offset+4:offset+12]=fp[-8:] 54 | 55 | # sign and patch the key 56 | sig = rsakey.sign(hash, Random.get_random_bytes(20)) 57 | 58 | sig=utils.get_int_bytes(sig) 59 | siglen=len(sig)*8 60 | 61 | patch=''.join([hash[-2:], 62 | struct.pack('!H',siglen), 63 | str(sig)]) 64 | key[hstart:hstart+len(patch)]=patch 65 | 66 | def loadkey(fname): 67 | try: 68 | inf=open(fname,'rb') 69 | except: 70 | print traceback.format_exc() 71 | print "error opening", fname 72 | help() 73 | key=bytearray(inf.read()) 74 | inf.close() 75 | return key 76 | 77 | def getrsaparams(key): 78 | if key[0]!=0x95: 79 | print "first param must be a secret key" 80 | sys.exit(1) 81 | 82 | offset=9 # start of key material 83 | n, offset = utils.get_mpi(key, offset) 84 | n=long(n) 85 | #print 'n', offset, "%x" % n 86 | e, offset = utils.get_mpi(key, offset) 87 | e=long(e) 88 | #print 'e', offset, "%x" % e 89 | d, offset = utils.get_mpi(key, offset+1) 90 | #print 'd', offset, "%x" % d 91 | p, offset = utils.get_mpi(key, offset) 92 | #print 'p', offset, "%x" % p 93 | q, offset = utils.get_mpi(key, offset) 94 | #print 'q', offset, "%x" % q 95 | u, offset = utils.get_mpi(key, offset) 96 | #print 'u', offset, "%x" % u 97 | return RSA.construct(([n, e, d, p, q, u])) 98 | 99 | def savekey(fname, key): 100 | try: 101 | outf=open(fname,'wb') 102 | except: 103 | print traceback.format_exc() 104 | print "error opening new key", fname 105 | help() 106 | outf.write(key) 107 | outf.close() 108 | 109 | def getnewfp(key, i): 110 | # patch date 111 | offset, length = old_tag_length(key,0) 112 | buffer=bytearray(''.join(['\x99', 113 | struct.pack('!H', length), 114 | str(key[offset+1:offset+1+length])])) 115 | buffer[4:8]=struct.pack('!i',i) 116 | #print "%02x %02x %02x %02x" % tuple(map(int,key[4:8])) 117 | m = hashlib.sha1() 118 | m.update(str(buffer)) 119 | print 'setting new fingerprint:', m.hexdigest()[-16:], i, datetime.datetime.fromtimestamp(i) 120 | return m.digest() 121 | 122 | if __name__ == "__main__": 123 | if len(sys.argv)!=4: help() 124 | # get new date 125 | try: 126 | i=int(sys.argv[3]) 127 | except: 128 | print traceback.format_exc() 129 | help() 130 | # load public key 131 | pkey=loadkey(sys.argv[2]) 132 | fp=getnewfp(pkey, i) 133 | 134 | # open secret key 135 | skey=loadkey(sys.argv[1]) 136 | rsakey=getrsaparams(skey) 137 | patchkey(skey, i, rsakey, fp) 138 | savekey(sys.argv[1]+'-new', skey) 139 | 140 | # patch public key 141 | patchkey(pkey, i, rsakey, fp) 142 | savekey(sys.argv[2]+'-new', pkey) 143 | -------------------------------------------------------------------------------- /genkey: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (c) 2012 s@ctrlc.hu 3 | # 4 | # This is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # invoke with ./genkey mynick 8192 5 10 | # 11 | # params: [] 12 | # 13 | # generates 14 | # - three passphrases (for the master and the 2 subkeys) 15 | # - an RSA 8192 signing master key, 16 | # - one encryting and one signing subkey 17 | # - an encrypted revocation cert for the masterkey 18 | # - the key for the revocation cert in 5 ssss shares 19 | # - an encrypted copy of the masterkey for backup 20 | # - the key for the backup in 5 ssss shares 21 | # 22 | # depends: gnupg, openssl, ssss, gpgsplit, srm 23 | 24 | gpg_opts='' 25 | source ${0%/*}/keytemplate 26 | 27 | wd="${1:-$(mktemp -d --tmpdir=/run/shm)}" 28 | 29 | function createkey { 30 | pubring="$wd/pubring.gpg" 31 | secring="$wd/secring.gpg" 32 | subkeys="$wd/subkeys.gpg" 33 | masterring="$wd/masterkey.gpg" 34 | cryptring="$wd/cryptkey.gpg" 35 | signring="$wd/signkey.gpg" 36 | backupring="$wd/backup.gpg" 37 | finalkey="$wd/finalkey.gpg" 38 | revokecert="$wd/revoke.gpg" 39 | revokes="$wd/revoke.shares" 40 | backups="$wd/backup.shares" 41 | 42 | echo "### Generating master key" 43 | read master 44 | set -x 45 | gpg $gpg_opts --gen-key --batch <( 46 | cat < $revokes 72 | # generate revokation cert for masterkey 73 | echo "y 74 | 1 75 | Revocation cert automatically generated at key generation time. 76 | 77 | y 78 | " | gpg $gpg_opts \ 79 | --command-fd 0 \ 80 | --no-default-keyring \ 81 | --secret-keyring $secring \ 82 | --keyring $pubring \ 83 | --gen-revoke $keyid | 84 | gpg $gpg_opts -c --batch --passphrase-file <(echo "$revokekey") > $revokecert 85 | 86 | set +x 87 | echo "### Creating a signing and encryption subkey" 88 | set -x 89 | 90 | echo \ 91 | "addkey 92 | 6 93 | $cryptkeysize 94 | $cryptexpire 95 | addkey 96 | 4 97 | $signkeysize 98 | $signexpire 99 | save 100 | " | gpg $gpg_opts \ 101 | --batch --yes \ 102 | --no-default-keyring \ 103 | --secret-keyring $secring \ 104 | --keyring $pubring \ 105 | --command-fd 0 \ 106 | --edit-key $keyid 107 | 108 | set +x 109 | echo "### Setting master passphrase" 110 | set -x 111 | mv $secring $masterring 112 | gpg $gpg_opts \ 113 | --batch --yes \ 114 | --no-default-keyring \ 115 | --secret-keyring $masterring \ 116 | --keyring $pubring \ 117 | --command-fd 0 \ 118 | --passphrase-file <(echo "$master") \ 119 | --passwd $keyid 120 | 121 | set +x 122 | echo "### Exporting subkeys" 123 | set -x 124 | gpg $gpg_opts \ 125 | --batch --yes \ 126 | --no-default-keyring \ 127 | --secret-keyring $masterring \ 128 | --keyring $pubring \ 129 | --passphrase-file <(echo "$master") \ 130 | --export-options export-reset-subkey-passwd \ 131 | --export-secret-subkeys $keyid \ 132 | > $subkeys 133 | 134 | set +x 135 | echo "### Importing subkeys into master-key-less new keyring" 136 | set -x 137 | gpg $gpg_opts \ 138 | --batch --yes \ 139 | --no-default-keyring \ 140 | --secret-keyring $secring \ 141 | --keyring $pubring \ 142 | --import $subkeys 143 | 144 | set +x 145 | echo "### Setting encryption password" 146 | set -x 147 | read pass1 148 | cp $secring $cryptring 149 | 150 | gpg $gpg_opts \ 151 | --batch --yes \ 152 | --no-default-keyring \ 153 | --secret-keyring $cryptring \ 154 | --keyring $pubring \ 155 | --command-fd 0 \ 156 | --passphrase-file <(echo "$pass1") \ 157 | --passwd $keyid 158 | 159 | gpgsplit -p "$wd/pass1" $cryptring 160 | 161 | set +x 162 | echo "### Setting signing password" 163 | set -x 164 | read pass2 165 | cp $secring $signring 166 | 167 | gpg $gpg_opts \ 168 | --batch --yes \ 169 | --no-default-keyring \ 170 | --secret-keyring $signring \ 171 | --keyring $pubring \ 172 | --command-fd 0 \ 173 | --passphrase-file <(echo "$pass2") \ 174 | --passwd $keyid 175 | 176 | gpgsplit -p "$wd/pass2" $signring 177 | 178 | cat "$wd/pass1000001-005.secret_key" \ 179 | "$wd/pass1000002-013.user_id" \ 180 | "$wd/pass1000003-002.sig" \ 181 | "$wd/pass1000004-012.ring_trust" \ 182 | "$wd/pass1000005-007.secret_subkey" \ 183 | "$wd/pass1000006-002.sig" \ 184 | "$wd/pass1000007-012.ring_trust" \ 185 | "$wd/pass2000008-007.secret_subkey" \ 186 | "$wd/pass2000009-002.sig" \ 187 | "$wd/pass2000010-012.ring_trust" \ 188 | > $finalkey 189 | set +x 190 | 191 | # srm only necessary if not working into tmpfs like volatile storage 192 | if which srm >/dev/null; then rm='srm -f'; 193 | elif which shred >/dev/null; then rm='shred -fu'; 194 | else rm='rm -f'; fi 195 | set -x 196 | $rm $wd/pass* $pubring~ $subkeys $secring $cryptring $signring 197 | 198 | set +x 199 | echo "### Generating a backup of the master key" 200 | set -x 201 | backupkey="$(openssl rand -hex 128)" 202 | # store password for revoke cert in ssss shares 203 | echo -n "$backupkey" | ssss-split -t $backupsharesneeded -n $backupshares -x -Q > $backups 204 | gpg $gpg_opts -c --batch --passphrase-file <(echo "$backupkey") -o $backupring $masterring 205 | set +x 206 | 207 | echo -- "------------------ keygen done ---------------------------" 208 | echo "Store your masterkey in a secure offline location" 209 | echo " $masterring" 210 | echo "Import your" 211 | echo " public key: $pubring" 212 | echo " subkeys: $finalkey" 213 | echo "" 214 | echo "Store the backup of your masterkey in" 215 | echo "a secure offline location different from the masterkey" 216 | echo " $backupring" 217 | echo "Store your revocation cert in a secure offline location" 218 | echo " $revokecert" 219 | echo "Distribute your revocation cert shares" 220 | echo " $revokes" 221 | echo "Store your backup shares in various secure locations" 222 | echo " $backups" 223 | echo "" 224 | echo "Learn your" 225 | echo " - master password:" 226 | echo " $master" 227 | echo " - encryption password:" 228 | echo " $pass1" 229 | echo " - signing password:" 230 | echo " $pass2" 231 | } 232 | 233 | # start some entropy generation 234 | #find / >/dev/null 2>&1 & 235 | #trap "kill $!" 0 1 2 3 15 236 | 237 | # create work directory if not existing 238 | [[ ! -e "$wd" ]] && { 239 | mkdir "$wd" || { 240 | echo "Oops, can't create workdir. Bailing out." 241 | exit 1 242 | } 243 | } 244 | [[ "$(stat -f -c %T $wd)" != "tmpfs" ]] && 245 | echo -e "Warning temporary keys should be stored in memory only\nFS of $wd is: $(stat -f -c %T "$wd")\n" 246 | 247 | # gen key 248 | { [[ -z "$masterpass" ]] && 249 | ${0%/*}/genphrase.py $masterphraselen || 250 | echo "$masterpass" 251 | [[ -z "$cryptpass" ]] && 252 | ${0%/*}/genphrase.py $cryptphraselen || 253 | echo "$cryptpass" 254 | [[ -z "$signpass" ]] && 255 | ${0%/*}/genphrase.py $signphraselen || 256 | echo "$signpass" 257 | } | createkey "$userid" 258 | --------------------------------------------------------------------------------