From 20388431aa8a1519649018ad55492a52f9ee9a33 Mon Sep 17 00:00:00 2001 From: brent s Date: Thu, 7 Sep 2017 16:36:26 -0400 Subject: [PATCH] BROKEN AF, in the middle of a rewrite --- gpg/kant/README | 15 + gpg/kant/commented.testbatch.kant.csv | 24 +- gpg/kant/kant.1 | 225 +++++++++ gpg/kant/kant.1.adoc | 102 +++- gpg/kant/kant.py | 689 +++++++++++++------------- gpg/kant/test.py | 62 +++ gpg/kant/testbatch.kant.csv | 10 +- gpg/sksdump.py | 2 +- 8 files changed, 755 insertions(+), 374 deletions(-) create mode 100644 gpg/kant/README create mode 100644 gpg/kant/kant.1 create mode 100755 gpg/kant/test.py diff --git a/gpg/kant/README b/gpg/kant/README new file mode 100644 index 0000000..a33faf6 --- /dev/null +++ b/gpg/kant/README @@ -0,0 +1,15 @@ +GENERATING THE MAN PAGE: +If you have asciidoctor installed, you can generate the manpage one of two ways. + +The first way: + + asciidoctor -b manpage kant.1.adoc -o- | groff -Tascii -man | gz -c > kant.1.gz + +This will generate a fixed-width man page. + + +The second way (recommended): + + asciidoctor -b manpage kant.1.adoc -o- | gz -c > kant.1.gz + +This will generate a dynamic-width man page. Most modern versions of man want this version. diff --git a/gpg/kant/commented.testbatch.kant.csv b/gpg/kant/commented.testbatch.kant.csv index 9811d99..c7b1962 100644 --- a/gpg/kant/commented.testbatch.kant.csv +++ b/gpg/kant/commented.testbatch.kant.csv @@ -1,14 +1,18 @@ # NOTE: The python csv module does NOT skip # commented lines! -# This is my personal key. Ultimate trust, push key. -748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1 +# This is my personal key. Ultimate trust, +# push key, careful checking, notify +748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1,3,1 # This is a testing junk key generated on a completely separate box, -# and does not exist on ANY keyservers. Never trust, Never push. -A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,-1 -# This is jthan's key. assign full trust, push to keyserver. -EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true -# This is paden's key. assign Marginal trust, push to keyserver. -6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True +# and does not exist on ANY keyservers nor the local keyring. +# Never trust, local sig, unknown checking, don't notify +A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,0,0,0 +# This is jthan's key. +# assign full trust, push to keyserver, casual checking, notify +EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true,casual,yes +# This is paden's key. +# assign Marginal trust, push to keyserver, casual checking, notify +6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True,Casual,True # This is the email for the Sysadministrivia serverkey. -# Assign full trust, push to keyserver. -, full, yes +# Assign full trust, push to keyserver, careful checking, don't notify +, full, yes, careful, false diff --git a/gpg/kant/kant.1 b/gpg/kant/kant.1 new file mode 100644 index 0000000..3cb714f --- /dev/null +++ b/gpg/kant/kant.1 @@ -0,0 +1,225 @@ +'\" t +.\" Title: kant +.\" Author: Brent Saner +.\" Generator: Asciidoctor 1.5.5 +.\" Date: 2017-09-07 +.\" Manual: KANT - Keysigning and Notification Tool +.\" Source: KANT +.\" Language: English +.\" +.TH "KANT" "1" "2017-09-07" "KANT" "KANT \- Keysigning and Notification Tool" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\\$2 \(laURL: \\$1 \(ra\\$3 +.. +.if \n[.g] .mso www.tmac +.LINKSTYLE blue R < > +.SH "NAME" +kant \- Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s) +.SH "SYNOPSIS" +.sp +\fBkant\fP [\fIOPTION\fP] \-k/\-\-key \fI\fP +.SH "OPTIONS" +.sp +Keysigning (and keysigning parties) can be a lot of fun, and can offer someone with new keys a way into the WoT (Web\-of\-Trust). +Unfortunately, they can be intimidating to those new to the experience. +This tool offers a simple and easy\-to\-use interface to sign public keys (normal, local\-only, and/or non\-exportable), +set owner trust, specify level of checking done, and push the signatures to a keyserver. It even supports batch operation via a CSV file. +.sp +\fB\-h\fP, \fB\-\-help\fP +.RS 4 +Display brief help/usage and exit. +.RE +.sp +\fB\-k\fP \fIKEY_IDS|BATCHFILE\fP, \fB\-\-key\fP \fIKEY_IDS|BATCHFILE\fP +.RS 4 +A single or comma\-separated list of key IDs (see \fBKEY ID FORMAT\fP) to sign, trust, and notify. Can also be an email address. +If \fB\-b\fP/\fB\-\-batch\fP is specified, this should instead be a path to the batch file (see \fBBATCHFILE/Format\fP). +.RE +.sp +\fB\-K\fP \fIKEY_ID\fP, \fB\-\-sigkey\fP \fIKEY_ID\fP +.RS 4 +The key to use when signing other keys (see \fBKEY ID FORMAT\fP). The default key is automatically determined at runtime +(it will be displayed in \fB\-h\fP/\fB\-\-help\fP output). +.RE +.sp +\fB\-t\fP \fITRUSTLEVEL\fP, \fB\-\-trust\fP \fITRUSTLEVEL\fP +.RS 4 +The trust level to automatically apply to all keys (if not specified, KANT will prompt for each key). +See \fBBATCHFILE/TRUSTLEVEL\fP for trust level notations. +.RE +.sp +\fB\-c\fP \fICHECKLEVEL\fP, \fB\-\-check\fP \fICHECKLEVEL\fP +.RS 4 +The level of checking that was done to confirm the validity of ownership for all keys being signed. If not specified, +the default is for KANT to prompt for each key we sign. See \fBBATCHFILE/CHECKLEVEL\fP for check level notations. +.RE +.sp +\fB\-l\fP \fILOCAL\fP, \fB\-\-local\fP \fILOCAL\fP +.RS 4 +If specified, make the signature(s) local\-only (i.e. non\-exportable, don\(cqt push to a keyserver). +See \fBBATCHFILE/LOCAL\fP for more information on local signatures. +.RE +.sp +\fB\-n\fP, \fB\-\-no\-notify\fP +.RS 4 +This requires some explanation. If you have MSMTP[1] installed and configured for the currently active user, +then we will send out emails to recipients letting them know we have signed their key. However, if MSMTP is installed and configured +but this flag is given, then we will NOT attempt to send emails. +.RE +.sp +\fB\-s\fP \fIKEYSERVER(S)\fP, \fB\-\-keyservers\fP \fIKEYSERVER(S)\fP +.RS 4 +The comma\-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime. +.RE +.sp +\fB\-b\fP, \fB\-\-batch\fP +.RS 4 +If specified, operate in batch mode. See \fBBATCHFILE\fP for more information. +.RE +.sp +\fB\-D\fP \fIGPGDIR\fP, \fB\-\-gpgdir\fP \fIGPGDIR\fP +.RS 4 +The GnuPG configuration directory to use (containing your keys, etc.). The default is automatically generated at runtime, +but will probably be \fB/home//.gnupg\fP or similar. +.RE +.sp +\fB\-T\fP, \fB\-\-testkeyservers\fP +.RS 4 +If specified, initiate a basic test connection with each set keyserver before anything else. Disabled by default. +.RE +.SH "KEY ID FORMAT" +.sp +Key IDs can be specified in one of two ways. The first (and preferred) way is to use the full 160\-bit (40\-character, hexadecimal) key ID. +A little known fact is the fingerprint of a key: +.sp +\fBDEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF\fP +.sp +is actually the full key ID of the primary key; i.e.: +.sp +\fBDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF\fP +.sp +The second way to specify a key, as far as KANT is concerned, is to use an email address. +Do note that if more than one key is found that matches the email address given (and they usually are), you will be prompted to select the specific +correct key ID anyways so it\(cqs usually a better idea to have the owner present their full key ID/fingerprint right from the get\-go. +.SH "BATCHFILE" +.SS "Format" +.sp +The batch file is a CSV\-formatted (comma\-delimited) file containing keys to sign and other information about them. It keeps the following format: +.sp +\fBKEY_ID,TRUSTLEVEL,LOCAL,CHECKLEVEL,NOTIFY\fP +.sp +For more information on each column, reference the appropriate sub\-section below. +.SS "KEY_ID" +.sp +See \fBKEY ID FORMAT\fP. +.SS "TRUSTLEVEL" +.sp +The \fITRUSTLEVEL\fP is specified by the following levels (you can use either the numeric or string representation): +.sp +.if n \{\ +.RS 4 +.\} +.nf +\fB\-1 = Never + 0 = Unknown + 1 = Untrusted + 2 = Marginal + 3 = Full + 4 = Ultimate\fP +.fi +.if n \{\ +.RE +.\} +.sp +It is how much trust to assign to a key, and the signatures that key makes on other keys.[2] +.SS "LOCAL" +.sp +Whether or not to push to a keyserver. It can be either the numeric or string representation of the following: +.sp +.if n \{\ +.RS 4 +.\} +.nf +\fB0 = False +1 = True\fP +.fi +.if n \{\ +.RE +.\} +.sp +If \fB1/True\fP, KANT will sign the key with a local signature (and the signature will not be pushed to a keyserver or be exportable).[3] +.SS "CHECKLEVEL" +.sp +The amount of checking that has been done to confirm that the owner of the key is who they say they are and that the key matches their provided information. +It can be either the numeric or string representation of the following: +.sp +.if n \{\ +.RS 4 +.\} +.nf +\fB0 = Unknown +1 = None +2 = Casual +3 = Careful\fP +.fi +.if n \{\ +.RE +.\} +.sp +It is up to you to determine the classification of the amount of checking you have done, but the following is recommended (it is the policy +the author follows): +.sp +.if n \{\ +.RS 4 +.\} +.nf +\fBUnknown:\fP The key is unknown and has not been reviewed + +\fBNone:\fP The key has been signed, but no confirmation of the + ownership of the key has been performed (typically + a local signature) + +\fBCasual:\fP The key has been presented and the owner is either + known to the signer or they have provided some form + of non\-government\-issued identification or other + proof (website, Keybase.io, etc.) + +\fBCareful:\fP The same as \fBCasual\fP requirements but they have + provided a government\-issued ID and all information + matches +.fi +.if n \{\ +.RE +.\} +.sp +It\(cqs important to check each key you sign carefully. Failure to do so may hurt others\(aq trust in your key.[4] +.SH "SEE ALSO" +.sp +gpg(1), gpgconf(1) +.SH "RESOURCES" +.sp +\fBAuthor\(cqs web site:\fP \c +.URL "https://square\-r00t.net/" "" "" +\fBAuthor\(cqs GPG information:\fP \c +.URL "https://square\-r00t.net/gpg\-info" "" "" +.SH "COPYING" +.sp +Copyright (C) 2017 Brent Saner. +.sp +Free use of this software is granted under the terms of the GPLv3 License. +.SH "NOTES" +1. http://msmtp.sourceforge.net/ +2. For more information on trust levels and the Web of Trust, see: https://www.gnupg.org/gph/en/manual/x334.html and https://www.gnupg.org/gph/en/manual/x547.html +3. For more information on pushing to keyservers and local signatures, see: https://www.gnupg.org/gph/en/manual/r899.html#LSIGN and https://lists.gnupg.org/pipermail/gnupg-users/2007-January/030242.html +4. GnuPG documentation refers to this as "validity"; see https://www.gnupg.org/gph/en/manual/x334.html +.SH "AUTHOR(S)" +.sp +\fBBrent Saner\fP +.RS 4 +Author(s). +.RE diff --git a/gpg/kant/kant.1.adoc b/gpg/kant/kant.1.adoc index 134202c..d9f7083 100644 --- a/gpg/kant/kant.1.adoc +++ b/gpg/kant/kant.1.adoc @@ -8,7 +8,7 @@ v1.0.0 == NAME -kant - Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s) +KANT - Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s) == SYNOPSIS @@ -26,25 +26,28 @@ set owner trust, specify level of checking done, and push the signatures to a ke *-k* _KEY_IDS|BATCHFILE_, *--key* _KEY_IDS|BATCHFILE_:: A single or comma-separated list of key IDs (see *KEY ID FORMAT*) to sign, trust, and notify. Can also be an email address. - If *-b*/*--batch* is specified, this should instead be a path to the batch file (see *BATCHFILE*). + If *-b*/*--batch* is specified, this should instead be a path to the batch file (see *BATCHFILE/Format*). *-K* _KEY_ID_, *--sigkey* _KEY_ID_:: The key to use when signing other keys (see *KEY ID FORMAT*). The default key is automatically determined at runtime (it will be displayed in *-h*/*--help* output). -*-t* _TRUSTLEVEL_, *--trustlevel* _TRUSTLEVEL_:: - The trust level to automatically apply to all keys (if not specified, kant will prompt for each key). See *BATCHFILE* for trust level notations. +*-t* _TRUSTLEVEL_, *--trust* _TRUSTLEVEL_:: + The trust level to automatically apply to all keys (if not specified, KANT will prompt for each key). + See *BATCHFILE/TRUSTLEVEL* for trust level notations. -*-c* _CHECKLEVEL_, *--checklevel* _CHECKLEVEL_:: +*-c* _CHECKLEVEL_, *--check* _CHECKLEVEL_:: The level of checking that was done to confirm the validity of ownership for all keys being signed. If not specified, - the default is for kant to prompt for each key we sign. See *BATCHFILE* for check level notations. - -*-e* _EXPORT_, *--export* _EXPORT_:: - Whether the signature(s) should be made exportable or not. See *BATCHFILE* for more information on exportability. - The default is True (signatures will be exportable). + the default is for KANT to prompt for each key we sign. See *BATCHFILE/CHECKLEVEL* for check level notations. *-l* _LOCAL_, *--local* _LOCAL_:: - Make the signature(s) local-only (i.e. don't push to a keyserver). + If specified, make the signature(s) local-only (i.e. non-exportable, don't push to a keyserver). + See *BATCHFILE/LOCAL* for more information on local signatures. + +*-n*, *--no-notify*:: + This requires some explanation. If you have MSMTPfootnote:[\http://msmtp.sourceforge.net/] installed and configured for the currently active user, + then we will send out emails to recipients letting them know we have signed their key. However, if MSMTP is installed and configured + but this flag is given, then we will NOT attempt to send emails. *-s* _KEYSERVER(S)_, *--keyservers* _KEYSERVER(S)_:: The comma-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime. @@ -52,7 +55,7 @@ set owner trust, specify level of checking done, and push the signatures to a ke *-b*, *--batch*:: If specified, operate in batch mode. See *BATCHFILE* for more information. -*-d* _GPGDIR_, *--gpgdir* _GPGDIR_:: +*-D* _GPGDIR_, *--gpgdir* _GPGDIR_:: The GnuPG configuration directory to use (containing your keys, etc.). The default is automatically generated at runtime, but will probably be */home//.gnupg* or similar. @@ -70,7 +73,7 @@ is actually the full key ID of the primary key; i.e.: *DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF* The second way to specify a key, as far as KANT is concerned, is to use an email address. -Do note that if more than one key is found that matches the email address given, you will be prompted to select the specific +Do note that if more than one key is found that matches the email address given (and they usually are), you will be prompted to select the specific correct key ID anyways so it's usually a better idea to have the owner present their full key ID/fingerprint right from the get-go. == BATCHFILE @@ -78,22 +81,85 @@ correct key ID anyways so it's usually a better idea to have the owner present t === Format The batch file is a CSV-formatted (comma-delimited) file containing keys to sign and other information about them. It keeps the following format: -*KEY_ID,TRUSTLEVEL,PUSH,CHECKLEVEL,EXPORT* +*KEY_ID,TRUSTLEVEL,LOCAL,CHECKLEVEL,NOTIFY* + +For more information on each column, reference the appropriate sub-section below. === KEY_ID See *KEY ID FORMAT*. === TRUSTLEVEL -The _TRUSTLEVEL_ is specified by the following levels: +The _TRUSTLEVEL_ is specified by the following levels (you can use either the numeric or string representation): - *THIS IS A TEST* +[subs=+quotes] +.... +*-1 = Never + 0 = Unknown + 1 = Untrusted + 2 = Marginal + 3 = Full + 4 = Ultimate* +.... + +It is how much trust to assign to a key, and the signatures that key makes on other keys.footnote:[For more information +on trust levels and the Web of Trust, see: \https://www.gnupg.org/gph/en/manual/x334.html and \https://www.gnupg.org/gph/en/manual/x547.html] + +=== LOCAL +Whether or not to push to a keyserver. It can be either the numeric or string representation of the following: + +[subs=+quotes] +.... +*0 = False +1 = True* +.... + +If *1/True*, KANT will sign the key with a local signature (and the signature will not be pushed to a keyserver or be exportable).footnote:[For +more information on pushing to keyservers and local signatures, see: \https://www.gnupg.org/gph/en/manual/r899.html#LSIGN and +\https://lists.gnupg.org/pipermail/gnupg-users/2007-January/030242.html] + +=== CHECKLEVEL +The amount of checking that has been done to confirm that the owner of the key is who they say they are and that the key matches their provided information. +It can be either the numeric or string representation of the following: + +[subs=+quotes] +.... +*0 = Unknown +1 = None +2 = Casual +3 = Careful* +.... + +It is up to you to determine the classification of the amount of checking you have done, but the following is recommended (it is the policy +the author follows): + +[subs=+quotes] +.... +*Unknown:* The key is unknown and has not been reviewed + +*None:* The key has been signed, but no confirmation of the + ownership of the key has been performed (typically + a local signature) + +*Casual:* The key has been presented and the owner is either + known to the signer or they have provided some form + of non-government-issued identification or other + proof (website, Keybase.io, etc.) + +*Careful:* The same as *Casual* requirements but they have + provided a government-issued ID and all information + matches +.... + +It's important to check each key you sign carefully. Failure to do so may hurt others' trust in your key.footnote:[GnuPG documentation refers +to this as "validity"; see \https://www.gnupg.org/gph/en/manual/x334.html] == SEE ALSO -gpg(1), gpgcong(1) +gpg(1), gpgconf(1) == RESOURCES -* Author's web site:* https://square-r00t.net/ +*Author's web site:* https://square-r00t.net/ +*Author's GPG information:* https://square-r00t.net/gpg-info == COPYING diff --git a/gpg/kant/kant.py b/gpg/kant/kant.py index ea82ae5..00a91bf 100755 --- a/gpg/kant/kant.py +++ b/gpg/kant/kant.py @@ -11,7 +11,9 @@ import subprocess from io import BytesIO from socket import * import urllib.parse -import gpgme # non-stdlib; Arch package is "python-pygpgme" +import gpg # non-stdlib; Arch package is "python-gpgme" - see + # https://git.archlinux.org/svntogit/packages.git/tree/trunk/PKGBUILD?h=packages/gpgme and + # https://pypi.python.org/pypi/gpg # TODO: # - http://tanguy.ortolo.eu/blog/article9/pgp-signature-infos edit certification level- possible with pygpgme? @@ -36,307 +38,323 @@ import gpgme # non-stdlib; Arch package is "python-pygpgme" # #Thanks again! -def getKeys(args): - # Get our concept - os.environ['GNUPGHOME'] = args['gpgdir'] - gpg = gpgme.Context() - keys = {} - allkeys = [] - # Do we have the key already? If not, fetch. - for k in args['rcpts'].keys(): - if args['rcpts'][k]['type'] == 'fpr': - allkeys.append(k) - if args['rcpts'][k]['type'] == 'email': - # We need to actually do a lookup on the email address. - with open(os.devnull, 'w') as f: - # TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms? - keyout = subprocess.run(['gpg2', - '--search-keys', - '--with-colons', - '--batch', - k], - stdout = subprocess.PIPE, - stderr = f) - keyout = keyout.stdout.decode('utf-8').splitlines() - for line in keyout: - if line.startswith('pub:'): - key = line.split(':')[1] - keys[key] = {} - keys[key]['uids'] = {} - keys[key]['time'] = int(line.split(':')[4]) - elif line.startswith('uid:'): - uid = re.split('<(.*)>', urllib.parse.unquote(line.split(':')[1].strip())) - uid.remove('') - uid = [u.strip() for u in uid] - keys[key]['uids'][uid[1]] = {} - keys[key]['uids'][uid[1]]['comment'] = uid[0] - keys[key]['uids'][uid[1]]['time'] = int(line.split(':')[2]) - if len(keys) > 1: # Print the keys and prompt for a selection. - print('\nWe found the following keys for <{0}>...\n\nKEY ID:'.format(k)) - for k in keys: - print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(k, '', datetime.datetime.utcfromtimestamp(keys[k]['time']))) - for email in keys[k]['uids']: - print('{0:42}(Generated {3}) <{2}> {1}'.format('', - keys[k]['uids'][email]['comment'], - email, - datetime.datetime.utcfromtimestamp(keys[k]['uids'][email]['time']))) +class sigsession(object): + def __init__(self, args): + self.args = args + + def getKeys(self): + # Get our concept + os.environ['GNUPGHOME'] = self.args['gpgdir'] + ctx = gpg.Context() + keys = {} + self.keyids = [] + # Do we have the key already? If not, fetch. + for k in list(self.args['rcpts']): + if self.args['rcpts'][k]['type'] == 'fpr': + self.keyids.append(k) + if self.args['rcpts'][k]['type'] == 'email': + # We need to actually do a lookup on the email address. + with open(os.devnull, 'w') as f: + # TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms? + keyout = subprocess.run(['gpg2', + '--search-keys', + '--with-colons', + '--batch', + k], + stdout = subprocess.PIPE, + stderr = f) + keyout = keyout.stdout.decode('utf-8').splitlines() + for line in keyout: + if line.startswith('pub:'): + key = line.split(':')[1] + keys[key] = {} + keys[key]['uids'] = {} + keys[key]['time'] = int(line.split(':')[4]) + elif line.startswith('uid:'): + uid = re.split('<(.*)>', urllib.parse.unquote(line.split(':')[1].strip())) + uid.remove('') + uid = [u.strip() for u in uid] + keys[key]['uids'][uid[1]] = {} + keys[key]['uids'][uid[1]]['comment'] = uid[0] + keys[key]['uids'][uid[1]]['time'] = int(line.split(':')[2]) + if len(keys) > 1: # Print the keys and prompt for a selection. + print('\nWe found the following keys for <{0}>...\n\nKEY ID:'.format(k)) + for k in keys: + print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(k, '', datetime.datetime.utcfromtimestamp(keys[k]['time']))) + for email in keys[k]['uids']: + print('{0:42}(Generated {3}) <{2}> {1}'.format('', + keys[k]['uids'][email]['comment'], + email, + datetime.datetime.utcfromtimestamp(keys[k]['uids'][email]['time']))) + print() + while True: + key = input('Please enter the (full) appropriate key: ') + if key not in keys.keys(): + print('Please enter a full key ID from the list above or hit ctrl-d to exit.') + else: + self.keyids.append(key) + break + else: + if not len(keys.keys()) >= 1: + print('Could not find {0}!'.format(k)) + del(self.args['rcpts'][k]) + continue + key = list(keys.keys())[0] + print('\nFound key {0} for {1} (Generated at {2}):'.format(key, k, datetime.datetime.utcfromtimestamp(keys[key]['time']))) + for email in keys[key]['uids']: + print('\t(Generated {2}) {0} <{1}>'.format(keys[key]['uids'][email]['comment'], + email, + datetime.datetime.utcfromtimestamp(keys[key]['uids'][email]['time']))) + self.keyids.append(key) print() - while True: - key = input('Please enter the (full) appropriate key: ') - if key not in keys.keys(): - print('Please enter a full key ID from the list above or hit ctrl-d to exit.') - else: - allkeys.append(key) - break + ## And now we can (FINALLY) fetch the key(s). + # TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms? + recvcmd = ['gpg2', '--recv-keys', '--batch', '--yes'] # We'll add the keys onto the end of this next. + recvcmd.extend(self.keyids) + with open(os.devnull, 'w') as f: + fetchout = subprocess.run(recvcmd, stdout = f, stderr = f) # We hide stderr because gpg, for some unknown reason, spits non-errors to stderr. + return(self.keyids) + + def trustKeys(self): + # Map the trust levels to "human" equivalent + trustmap = {-1: ['never', gpgme.VALIDITY_NEVER], # this is... probably? not ideal, but. + 0: ['unknown', gpgme.VALIDITY_UNKNOWN], + 1: ['untrusted', gpgme.VALIDITY_UNDEFINED], + 2: ['marginal', gpgme.VALIDITY_MARGINAL], + 3: ['full', gpgme.VALIDITY_FULL], + 4: ['ultimate', gpgme.VALIDITY_ULTIMATE]} + locmap = {0: ['no', False], + 1: ['yes', True]} + def promptTrust(kinfo): + for k in list(kinfo): + if 'trust' not in kinfo[k].keys(): + trust_lvl = None + trust_in = input(('\nWhat trust level should we assign to {0}?\n\t\t\t\t ({1} <{2}>)' + + '\n\n\t\033[1m-1 = Never\n\t 0 = Unknown\n\t 1 = Untrusted\n\t 2 = Marginal\n\t 3 = Full' + + '\n\t 4 = Ultimate\033[0m\nTrust: ').format(k, kinfo[k]['name'], kinfo[k]['email'])) + for dictk, dictv in trustmap.items(): + if trust_in.lower().strip() == dictv[0]: + trust_lvl = int(dictk) + elif trust_in == str(dictk): + trust_lvl = int(dictk) + if not trust_lvl: + print('Not a valid trust level; skipping. Run kant again to fix.') + continue + kinfo[k]['trust'] = trustmap[trust_lvl][1] + if 'local' not in kinfo[k].keys(): + local = False + if args['keyservers']: + local_in = input('\nShould we push {0} to the keyserver(s) (\033[1mYES\033[0m/No)? '.format(k)) + if local_in.lower().startswith('n'): + local = True + kinfo[k]['local'] = local + return(kinfo) + os.environ['GNUPGHOME'] = args['gpgdir'] + gpg = gpgme.Context() + # Build out some info about keys + kinfo = {} + for k in self.keyids: + if k not in kinfo.keys(): + kinfo[k] = {} else: - if not len(keys.keys()) >= 1: - print('Could not find {0}!'.format(k)) - del(args['rcpts'][k]) - continue - key = list(keys.keys())[0] - print('\nFound key {0} for {1} (Generated at {2}):'.format(key, k, datetime.datetime.utcfromtimestamp(keys[key]['time']))) - for email in keys[key]['uids']: - print('\t(Generated {2}) {0} <{1}>'.format(keys[key]['uids'][email]['comment'], - email, - datetime.datetime.utcfromtimestamp(keys[key]['uids'][email]['time']))) - allkeys.append(key) - print() - ## And now we can (FINALLY) fetch the key(s). - # TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms? - recvcmd = ['gpg2', '--recv-keys', '--batch', '--yes'] # We'll add the keys onto the end of this next. - recvcmd.extend(allkeys) - with open(os.devnull, 'w') as f: - fetchout = subprocess.run(recvcmd, stdout = f, stderr = f) # We hide stderr because gpg, for some unknown reason, spits non-errors to stderr. - return(allkeys) - -def trustKeys(keyids, args): - # Map the trust levels to "human" equivalent - trustmap = {-1: ['never', gpgme.VALIDITY_NEVER], # this is... probably? not ideal, but. - 0: ['unknown', gpgme.VALIDITY_UNKNOWN], - 1: ['untrusted', gpgme.VALIDITY_UNDEFINED], - 2: ['marginal', gpgme.VALIDITY_MARGINAL], - 3: ['full', gpgme.VALIDITY_FULL], - 4: ['ultimate', gpgme.VALIDITY_ULTIMATE]} - pushmap = {-1: ['never', None], - 0: ['no', False], - 1: ['yes', True]} - def promptTrust(kinfo): - for k in list(kinfo): - if 'trust' not in kinfo[k].keys(): - trust_lvl = None - trust_in = input(('\nWhat trust level should we assign to {0}?\n\t\t\t\t ({1} <{2}>)' + - '\n\n\t\033[1m-1 = Never\n\t 0 = Unknown\n\t 1 = Untrusted\n\t 2 = Marginal\n\t 3 = Full' + - '\n\t 4 = Ultimate\033[0m\nTrust: ').format(k, kinfo[k]['name'], kinfo[k]['email'])) - for dictk, dictv in trustmap.items(): - if trust_in.lower().strip() == dictv[0]: - trust_lvl = int(dictk) - elif trust_in == str(dictk): - trust_lvl = dictk - if not trust_lvl: - print('Not a valid trust level; skipping. Run kant again to fix.') - continue - kinfo[k]['trust'] = trustmap[trust_lvl][1] - if 'push' not in kinfo[k].keys(): - push = True - if args['keyservers']: - push_in = input('\nShould we push {0} to the keyserver(s) (\033[1mYES\033[0m/No/Never)? '.format(k)) - if push_in.lower() == 'never': - push = None - elif push_in.lower().startswith('n'): - push = False - kinfo[k]['push'] = push - return(kinfo) - os.environ['GNUPGHOME'] = args['gpgdir'] - gpg = gpgme.Context() - # Build out some info about keys - kinfo = {} - for k in keyids: - if k not in kinfo.keys(): - kinfo[k] = {} + continue # The key was already parsed; don't waste time adding the info + try: + kobj = gpg.get_key(k) + kinfo[k]['name'] = kobj.uids[0].name + kinfo[k]['email'] = kobj.uids[0].email + except gpgme.GpgmeError: + print('Can\'t get information about key {0}; skipping.'.format(k)) + del(kinfo[k]) + if not args['batch']: + if not args['trustlevel']: + self.trusts = promptTrust(kinfo) + else: + for k in list(kinfo): + local = False + if 'trust' not in kinfo[k].keys(): + for dictk, dictv in trustmap.items(): + if args['trustlevel'].lower().strip() == dictv[0]: + trust_lvl = int(dictk) + elif args['trustlevel'] == str(dictk): + trust_lvl = int(dictk) + if not trust_lvl: + print('Not a valid trust level; skipping. Run kant again to fix.') + continue + if 'local' not in kinfo[k].keys(): + if args['local']: + local = True + kinfo[k]['local'] = local + kinfo[k]['trust'] = trustmap[trust_lvl][1] + self.trusts = kinfo else: - continue # The key was already parsed; don't waste time adding the info - try: - kobj = gpg.get_key(k) - kinfo[k]['name'] = kobj.uids[0].name - kinfo[k]['email'] = kobj.uids[0].email - except gpgme.GpgmeError: - print('Can\'t get information about key {0}; skipping.'.format(k)) - del(kinfo[k]) - if not args['batch']: - trusts = promptTrust(kinfo) - else: - trusts = {} - csvd = {} # We import the CSV into a totally separate dict so we can do some validation loops - with open(args['keys'], 'r') as f: - for row in csv.reader(f, delimiter = ',', quotechar = '"'): - csvd[row[0]] = {'trust': row[1], 'push': row[2]} - for k in list(csvd): - if re.match('^?$', k): # is it an email address? - fullkey = gpg.get_key(k) - csvd[fullkey.subkeys[0].fpr] = csvd[k] - del(csvd[k]) - k = fullkey.subkeys[0].fpr - if k not in trusts.keys(): - trusts[k] = {} - if 'trust' not in trusts[k].keys(): - # Properly index the trust - strval = str(csvd[k]['trust']).lower().strip() - if strval == 'true': - trusts[k]['trust'] = True - elif strval == 'false': - trusts[k]['trust'] = False - elif strval == 'none': - trusts[k]['trust'] = None - else: - for dictk, dictv in trustmap.items(): # "no"/"yes" - if strval == dictv[0]: - trusts[k]['trust'] = trustmap[dictk][1] - elif strval == str(dictk): - trusts[k]['trust'] = trustmap[dictk][1] - if 'trust' not in trusts[k].keys(): # yes, again. we make sure it was set. otherwise, we need to skip this key. - print('Key {0}: trust level "{1}" is invalid; skipping.'.format(k, csvd[k]['trust'])) - del(trusts[k]) + self.trusts = {} + csvd = {} # We import the CSV into a totally separate dict so we can do some validation loops + with open(self.args['keys'], 'r') as f: + for row in csv.reader(f, delimiter = ',', quotechar = '"'): + csvd[row[0]] = {'trust': row[1].strip(), + 'local': row[2].strip(), + 'check': row[3].strip(), + 'notify': row[4].strip()} + for k in list(csvd): + if re.match('^?$', k): # is it an email address? + fullkey = gpg.get_key(k) + csvd[fullkey.subkeys[0].fpr] = csvd[k] + del(csvd[k]) + k = fullkey.subkeys[0].fpr + if k not in trusts.keys(): + self.trusts[k] = {} + if 'trust' not in trusts[k].keys(): + # Properly index the trust + strval = str(csvd[k]['trust']).lower().strip() + if strval == 'true': + self.trusts[k]['trust'] = True + elif strval == 'false': + self.trusts[k]['trust'] = False + elif strval == 'none': + self.trusts[k]['trust'] = None + else: + for dictk, dictv in trustmap.items(): + if strval == dictv[0]: + self.trusts[k]['trust'] = trustmap[dictk][1] + elif strval == str(dictk): + self.trusts[k]['trust'] = trustmap[dictk][1] + if 'trust' not in self.trusts[k].keys(): # yes, again. we make sure it was set. otherwise, we need to skip this key. + print('Key {0}: trust level "{1}" is invalid; skipping.'.format(k, csvd[k]['trust'])) + del(self.trusts[k]) + continue + # Now we need to index whether we push or not. + if 'local' not in self.trusts[k].keys(): + strval = str(csvd[k]['local']).lower().strip() + if strval == 'true': + self.trusts[k]['local'] = True + elif strval == 'false': + self.trusts[k]['local'] = False + else: + for dictk, dictv in locmap.items(): + if strval in dictv[0]: + self.trusts[k]['local'] = locmap[dictk][1] + elif strval == str(dictk): + self.trusts[k]['local'] = locmap[dictk][1] + if 'local' not in self.trusts[k].keys(): # yep. double-check + print('Key {0}: local option "{1}" is invalid; skipping.'.format(k, csvd[k]['local'])) + del(self.trusts[k]) + continue + # WHEW. THAT'S A LOT OF VALIDATIONS. Now the Business-End(TM) + # Reverse mapping of constants to human-readable + rmap = {gpgme.VALIDITY_NEVER: 'Never', + gpgme.VALIDITY_UNKNOWN: 'Unknown', + gpgme.VALIDITY_UNDEFINED: 'Untrusted', + gpgme.VALIDITY_MARGINAL: 'Marginal', + gpgme.VALIDITY_FULL: 'Full', + gpgme.VALIDITY_ULTIMATE: 'Ultimate'} + mykey = gpg.get_key(args['sigkey']) + for k in list(self.trusts): + keystat = None + try: + tkey = gpg.get_key(k) + except gpgme.GpgmeError: + print('Cannot find {0} in keyring at all; skipping.'.format(k)) + del(self.trusts[k]) continue - # Now we need to index whether we push or not. - if 'push' not in trusts[k].keys(): - strval = str(csvd[k]['push']).lower().strip() - if strval == 'true': - trusts[k]['push'] = True - elif strval == 'false': - trusts[k]['push'] = False - elif strval == 'none': - trusts[k]['push'] = None - else: - for dictk, dictv in pushmap.items(): # "no"/"yes" - if strval in dictv[0]: - trusts[k]['push'] = pushmap[dictk][1] - elif strval == str(dictk): - trusts[k]['push'] = pushmap[dictk][1] - if 'push' not in trusts[k].keys(): # yep. double-check - print('Key {0}: push option "{1}" is invalid; skipping.'.format(k, csvd[k]['push'])) - del(trusts[k]) - continue - # WHEW. THAT'S A LOT OF VALIDATIONS. Now the Business-End(TM) - # Reverse mapping of constants to human-readable - rmap = {gpgme.VALIDITY_NEVER: 'Never', - gpgme.VALIDITY_UNKNOWN: 'Unknown', - gpgme.VALIDITY_UNDEFINED: 'Untrusted', - gpgme.VALIDITY_MARGINAL: 'Marginal', - gpgme.VALIDITY_FULL: 'Full', - gpgme.VALIDITY_ULTIMATE: 'Ultimate'} - mykey = gpg.get_key(args['sigkey']) - for k in list(trusts): - keystat = None - try: - tkey = gpg.get_key(k) - except gpgme.GpgmeError: - print('Cannot find {0} in keyring at all; skipping.'.format(k)) - del(trusts[k]) - continue - curtrust = rmap[tkey.owner_trust] - newtrust = rmap[trusts[k]['trust']] - if tkey.owner_trust == trusts[k]['trust']: - trusts[k]['change'] = False - continue # Don't bother; we aren't changing the trust level, it's the same (OR we haven't trusted yet) - elif tkey.owner_trust == gpgme.VALIDITY_UNKNOWN: - keystat = 'a NEW TRUST' - elif tkey.owner_trust > trusts[k]['trust']: - keystat = 'a DOWNGRADE' - elif tkey.owner_trust < trusts[k]['trust']: - keystat = 'an UPGRADE' - print(('\nKey 0x{0} [{1} ({2})]:\n' + - '\tThis trust level ({3}) is {4} from the current trust level ({5}).').format(k, - kinfo[k]['name'], - kinfo[k]['email'], - newtrust, - keystat, - curtrust)) - tchk = input('Continue? (yes/\033[1mNO\033[0m) ') - if tchk.lower().startswith('y'): - trusts[k]['change'] = True - else: - trusts[k]['change'] = False - for k in list(trusts): - if trusts[k]['change']: - gpgme.editutil.edit_trust(gpg, k, trusts['k']['trust']) - print() - return(trusts) + curtrust = rmap[tkey.owner_trust] + newtrust = rmap[self.trusts[k]['trust']] + if tkey.owner_trust == trusts[k]['trust']: + self.trusts[k]['change'] = False + continue # Don't bother; we aren't changing the trust level, it's the same (OR we haven't trusted yet) + elif tkey.owner_trust == gpgme.VALIDITY_UNKNOWN: + keystat = 'a NEW TRUST' + elif tkey.owner_trust > trusts[k]['trust']: + keystat = 'a DOWNGRADE' + elif tkey.owner_trust < trusts[k]['trust']: + keystat = 'an UPGRADE' + print(('\nKey {0} [{1} ({2})]:\n' + + '\tThis trust level ({3}) is {4} from the current trust level ({5}).').format(k, + kinfo[k]['name'], + kinfo[k]['email'], + newtrust, + keystat, + curtrust)) + tchk = input('Continue? (yes/\033[1mNO\033[0m) ') + if tchk.lower().startswith('y'): + self.trusts[k]['change'] = True + else: + self.trusts[k]['change'] = False + for k in list(self.trusts): + if self.trusts[k]['change']: + print(k) + gpg.editutil.edit_trust(ctx, ctx.get_key(k), self.trusts[k]['trust']) + print() + return(self.trusts) -def sigKeys(trusts, args): # The More Business-End(TM) - import pprint - pprint.pprint(trusts) - os.environ['GNUPGHOME'] = args['gpgdir'] - gpg = gpgme.Context() - gpg.keylist_mode = gpgme.KEYLIST_MODE_SIGS - mkey = gpg.get_key(args['sigkey']) - gpg.signers = [mkey] - global_policy = {} - global_policy['push'] = True # I may be able to provide a way to explicitly change this at runtime later - global_policy['sign'] = True - if not args['keyservers']: - global_policy['sign'] = 'local' - global_policy['push'] = False - for k in list(trusts): - sign = True - key = gpg.get_key(k) - for uid in key.uids: - for s in uid.signatures: - try: - signerkey = gpg.get_key(s.keyid).subkeys[0].fpr - if signerkey == mkey.subkeys[0].fpr: - sign = False # We already signed this key - except gpgme.GpgmeError: - pass # usually if we get this it means we don't have a signer's key in our keyring - trusts[k]['sign'] = sign + def sigKeys(self): # The More Business-End(TM) + os.environ['GNUPGHOME'] = args['gpgdir'] + ctx = gpg.Context() + ctx.keylist_mode = gpg.KEYLIST_MODE_SIGS + mkey = ctx.get_key(args['sigkey']) + ctx.signers = [mkey] + global_policy = {} + for k in list(self.trusts): + sign = True + key = ctx.get_key(k) + for uid in key.uids: + for s in uid.signatures: + try: + signerkey = ctx.get_key(s.keyid).subkeys[0].fpr + if signerkey == mkey.subkeys[0].fpr: + sign = False # We already signed this key + except gpgme.GpgError: + pass # usually if we get this it means we don't have a signer's key in our keyring + self.trusts[k]['sign'] = sign + import pprint + pprint.pprint(self.trusts) + # edit_sign(ctx, key, index=0, local=False, norevoke=False, expire=True, check=0) + # index: the index of the user ID to sign, starting at 1. Sign all + # user IDs if set to 0. + # local: make a local signature + # norevoke: make a non-revokable signature + # command: the type of signature. One of sign, lsign, tsign or nrsign. + # expire: whether the signature should expire with the key. + # check: Amount of checking performed. One of: + # 0 - no answer + # 1 - no checking + # 2 - casual checking + # 3 - careful checking - # edit_sign(ctx, key, index=0, local=False, norevoke=False, expire=True, check=0) - # index: the index of the user ID to sign, starting at 1. Sign all - # user IDs if set to 0. - # local: make a local signature - # norevoke: make a non-revokable signature - # command: the type of signature. One of sign, lsign, tsign or nrsign. - # expire: whether the signature should expire with the key. - # check: Amount of checking performed. One of: - # 0 - no answer - # 1 - no checking - # 2 - casual checking - # 3 - careful checking - - #gpgme.editutil.edit_sign(gpg, k, index = 0, lo + #gpgme.editutil.edit_sign(gpg, k, index = 0, lo -def pushKeys(): # The Last Business-End(TM) - pass + def pushKeys(): # The Last Business-End(TM) + pass -def modifyDirmngr(op, args): - if not args['keyservers']: - return() - pid = str(os.getpid()) - activecfg = os.path.join(args['gpgdir'], 'dirmngr.conf') - bakcfg = '{0}.{1}'.format(activecfg, pid) - if op in ('new', 'start'): - if os.path.lexists(activecfg): - shutil.copy2(activecfg, bakcfg) - with open(bakcfg, 'r') as read, open(activecfg, 'w') as write: - for line in read: - if not line.startswith('keyserver '): - write.write(line) - with open(activecfg, 'a') as f: - for s in args['keyservers']: - uri = '{0}://{1}:{2}'.format(s['proto'], s['server'], s['port'][0]) - f.write('keyserver {0}\n'.format(uri)) - if op in ('old', 'stop'): - if os.path.lexists(bakcfg): + def modifyDirmngr(self, op): + if not self.args['keyservers']: + return() + pid = str(os.getpid()) + activecfg = os.path.join(self.args['gpgdir'], 'dirmngr.conf') + bakcfg = '{0}.{1}'.format(activecfg, pid) + if op in ('new', 'start'): + if os.path.lexists(activecfg): + shutil.copy2(activecfg, bakcfg) with open(bakcfg, 'r') as read, open(activecfg, 'w') as write: for line in read: - write.write(line) - os.remove(bakcfg) - else: - os.remove(activecfg) - subprocess.run(['gpgconf', - '--reload', - 'dirmngr']) - return() + if not line.startswith('keyserver '): + write.write(line) + with open(activecfg, 'a') as f: + for s in self.args['keyservers']: + uri = '{0}://{1}:{2}'.format(s['proto'], s['server'], s['port'][0]) + f.write('keyserver {0}\n'.format(uri)) + if op in ('old', 'stop'): + if os.path.lexists(bakcfg): + with open(bakcfg, 'r') as read, open(activecfg, 'w') as write: + for line in read: + write.write(line) + os.remove(bakcfg) + else: + os.remove(activecfg) + subprocess.run(['gpgconf', + '--reload', + 'dirmngr']) + return() def serverParser(uri): # https://en.wikipedia.org/wiki/Key_server_(cryptographic)#Keyserver_examples @@ -385,8 +403,8 @@ def parseArgs(): if not defgpgdir: return(None) defkey = None - gpg = gpgme.Context() - for k in gpg.keylist(None, True): # params are query and secret keyring, respectively + ctx = gpg.Context() + for k in ctx.keylist(None, True): # params are query and secret keyring, respectively if k.can_sign and True not in (k.revoked, k.expired, k.disabled): defkey = k.subkeys[0].fpr break # We'll just use the first primary key we find that's valid as the default. @@ -410,71 +428,61 @@ def parseArgs(): defkey = getDefKey(defgpgdir) defkeyservers = getDefKeyservers(defgpgdir) args = argparse.ArgumentParser(description = 'Keysigning Assistance and Notifying Tool (KANT)', - epilog = 'brent s. || 2017 || https://square-r00t.net', - formatter_class = argparse.RawTextHelpFormatter) + epilog = 'brent s. || 2017 || https://square-r00t.net') args.add_argument('-k', '--keys', dest = 'keys', metavar = 'KEYS | /path/to/batchfile', required = True, - help = 'A single or comma-separated list of keys to sign,\n' + - 'trust, and notify. Can also be an email address.\n' + - 'If -b/--batch is specified, this should instead be\n' + - 'a path to the batch file.') + help = 'A single/comma-separated list of keys to sign, ' + + 'trust, & notify. Can also be an email address. ' + + 'If -b/--batch is specified, this should instead be ' + + 'a path to the batch file. See the man page for more info.') args.add_argument('-K', '--sigkey', dest = 'sigkey', default = defkey, - help = 'The key to use when signing other keys.\nDefault is \033[1m{0}\033[0m.'.format(defkey)) + help = 'The key to use when signing other keys. Default is \033[1m{0}\033[0m.'.format(defkey)) args.add_argument('-t', '--trust', dest = 'trustlevel', default = None, - help = 'The trust level to automatically apply to all keys\n' + - '(if not specified, kant will prompt for each key).\n' + - 'See -b/--batch for trust level notations.') + help = 'The trust level to automatically apply to all keys ' + + '(if not specified, kant will prompt for each key). ' + + 'See BATCHFILE/TRUSTLEVEL in the man page for trust ' + + 'level notations.') args.add_argument('-c', '--check', dest = 'checklevel', default = None, - help = 'The level of checking done (if not specified, kant will\n' + + help = 'The level of checking done (if not specified, kant will ' + 'prompt for each key). See -b/--batch for check level notations.') - args.add_argument('-e', - '--export', - dest = 'export', - default = 'true', - help = 'Make the signatures exportable (default is True).\nSee -b/--batch for more information.') args.add_argument('-l', '--local', dest = 'local', default = 'false', help = 'Make the signature(s) local-only (i.e. don\'t push to a keyserver).') + args.add_argument('-n', + '--no-notify', + dest = 'notify', + action = 'store_false', + help = 'If specified, do NOT notify any key recipients that you\'ve signed ' + + 'their key, even if KANT is able to.') args.add_argument('-s', '--keyservers', dest = 'keyservers', default = defkeyservers, help = 'The comma-separated keyserver(s) to push to.\n' + - 'Default keyserver list is: \n\n\t\033[1m{0}\033[0m\n\n'.format(re.sub(',', '\n\t', defkeyservers))) - # This will require some restructuring... + 'Default keyserver list is: \n\n\t\033[1m{0}\033[0m\n\n'.format(re.sub(',', + '\n\t', + defkeyservers))) args.add_argument('-b', '--batch', dest = 'batch', action = 'store_true', - help = 'If specified, -k/--keys is a CSV file to use as a\n' + - 'batch run in the format of (one per line):\n' + - '\n\033[1mKEY_ID,TRUSTLEVEL,PUSH,CHECKLEVEL,EXPORT\033[0m\n' - '\n\033[1mKEY_ID\033[0m can be the full 40-char key ID (fingerprint)\n' + - 'or an email address of the key.\n\n\033[1mTRUSTLEVEL\033[0m is how much trust to assign, and can\n' + - 'be numeric or string:' + - '\n\n\t\033[1m-1 = Never\n\t 0 = Unknown\n\t 1 = Untrusted\n\t 2 = Marginal\n\t 3 = Full\n\t 4 = Ultimate\033[0m\n' + - '\n\033[1mPUSH\033[0m can be \033[1m1/True\033[0m or \033[1m0/False\033[0m.\n' + - 'If marked as False, the signature will be made local.\n' + - '\n\033[1mCHECKLEVEL\033[0m is the amount of checking done on the owner\'s\n' + - 'validity of identity. Can be numeric or string:' + - '\n\n\t\033[1m 0 = Unknown\n\t 1 = None\n\t 2 = Casual\n\t 3 = Careful\033[0m\n' + - '\n\033[1mEXPORT\033[0m can be either \033[1m1/True\033[0m or \033[1m0/False\033[0m.\n' + - 'If True, make the signature exportable.\nIf False, make it non-exportable.') - args.add_argument('-d', + help = 'If specified, -k/--keys is a CSV file to use as a ' + + 'batch run. See the BATCHFILE section in the man page for more info.') + args.add_argument('-D', '--gpgdir', dest = 'gpgdir', default = defgpgdir, @@ -533,12 +541,12 @@ def verifyArgs(args): raise NotADirectoryError('{0} is not a directory'.format(args['gpgdir'])) try: os.environ['GNUPGHOME'] = args['gpgdir'] - gpg = gpgme.Context() + ctx = gpg.Context() except: raise RuntimeError('Could not use {0} as a GnuPG home'.format(args['gpgdir'])) # Now we need to verify that the private key exists... try: - sigkey = gpg.get_key(args['sigkey'], True) + sigkey = ctx.get_key(args['sigkey'], True) except GpgmeError: raise ValueError('Cannot use key {0}'.format(args['sigkey'])) # And that it is an eligible candidate to use to sign. @@ -548,12 +556,12 @@ def verifyArgs(args): if args['testkeyservers']: for s in args['keyservers']: # Test to make sure the keyserver is accessible. - # First we need to construct a way to use python's socket connector - # Great. Now we need to just quickly check to make sure it's accessible - if specified. - if args['netproto'] == '4': - nettype = AF_INET - elif args['netproto'] == '6': - nettype = AF_INET6 + v6test = socket(AF_INET6, SOCK_DGRAM) + try: + v6test.connect(('ipv6.square-r00t.net', 0)) + nettype = AF_INET6 # We have IPv6 intarwebz + except: + nettype = AF_INET # No IPv6, default to IPv4 for proto in s['port'][1]: if proto == 'udp': netproto = SOCK_DGRAM @@ -564,7 +572,7 @@ def verifyArgs(args): tests = sock.connect_ex((s['server'], int(s['port'][0]))) uristr = '{0}://{1}:{2} ({3})'.format(s['proto'], s['server'], s['port'][0], proto.upper()) if not tests == 0: - raise RuntimeError('Keyserver {0} is not available'.format(uristr)) + raise OSError('Keyserver {0} is not available'.format(uristr)) else: print('Keyserver {0} is accepting connections.'.format(uristr)) sock.close() @@ -573,11 +581,12 @@ def verifyArgs(args): def main(): rawargs = parseArgs() args = verifyArgs(vars(rawargs.parse_args())) - modifyDirmngr('new', args) - fprs = getKeys(args) - trusts = trustKeys(fprs, args) - sigs = sigKeys(trusts, args) - modifyDirmngr('old', args) + sess = sigsession(args) + sess.modifyDirmngr('new') + sess.getKeys() + sess.trustKeys() + sess.sigKeys() + sess.modifyDirmngr('old') if __name__ == '__main__': main() diff --git a/gpg/kant/test.py b/gpg/kant/test.py new file mode 100755 index 0000000..c32048a --- /dev/null +++ b/gpg/kant/test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# This is more of a documentation on some python-gpgme (https://pypi.python.org/pypi/gpg) examples. +# Because their only documentation for the python bindings is in pydoc, and the C API manual is kind of useless. + +import gpg +import gpg.constants +import inspect +import pprint + +# my key ID +mykey = '748231EBCBD808A14F5E85D28C004C2F93481F6B' +# a key to test with +theirkey = '63D1CEA387C27A92E0D50AB8343C305F9109D4DC' + +# Create a context +# Params: +#armor -- enable ASCII armoring (default False) +#textmode -- enable canonical text mode (default False) +#offline -- do not contact external key sources (default False) +#signers -- list of keys used for signing (default []) +#pinentry_mode -- pinentry mode (default PINENTRY_MODE_DEFAULT) +#protocol -- protocol to use (default PROTOCOL_OpenPGP) +#home_dir -- state directory (default is the engine default) +ctx = gpg.Context() + +# Fetch a key from the keyring +#secret -- to request a secret key +mkey = ctx.get_key(mykey) +tkey = ctx.get_key(theirkey) + +## Print the attributes of our key and other info +##https://stackoverflow.com/a/41737776 +##for k in (mkey, tkey): +#for k in [mkey]: +# for i in inspect.getmembers(k): +# if not i[0].startswith('_'): +# pprint.pprint(i) +#pprint.pprint(ctx.get_engine_info()) + +# Print the constants +#pprint.pprint(inspect.getmembers(gpg.constants)) + +# Get remote key. Use an OR to search both keyserver and local. +#ctx.set_keylist_mode(gpg.constants.KEYLIST_MODE_EXTERN|gpg.constants.KEYLIST_MODE_LOCAL) +klmodes = {'local': gpg.constants.KEYLIST_MODE_LOCAL, + 'remote': gpg.constants.KEYLIST_MODE_EXTERN, + 'both': gpg.constants.KEYLIST_MODE_LOCAL|gpg.constants.KEYLIST_MODE_EXTERN} + +# List keys +#pattern -- return keys matching pattern (default: all keys) +#secret -- return only secret keys (default: False) +#mode -- keylist mode (default: list local keys) +#source -- read keys from source instead from the keyring +# (all other options are ignored in this case) +ctx.keylist(pattern = 'bts@square-r00t.net', + secret = False, + mode = klmodes['both'], + source = None) + +# Test fetching from a keyserver + diff --git a/gpg/kant/testbatch.kant.csv b/gpg/kant/testbatch.kant.csv index b6b8d57..e26131f 100644 --- a/gpg/kant/testbatch.kant.csv +++ b/gpg/kant/testbatch.kant.csv @@ -1,5 +1,5 @@ -748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1 -A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,-1 -EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true -6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True -, full, yes +748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1,3,1 +A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,0,0,0 +EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true,casual,yes +6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True,Casual,True +, full, yes, careful, false diff --git a/gpg/sksdump.py b/gpg/sksdump.py index 1f80d64..8f87463 100755 --- a/gpg/sksdump.py +++ b/gpg/sksdump.py @@ -42,7 +42,7 @@ sks = { # I would hope this is self-explanatory. If not, this is where we log the outout of the sks dump process. (and any rsync errors, too) 'logfile': '/var/log/sksdump.log', # If not None value, where we should push the dumps when done. Can be a local path too, obviously. - 'rsync': 'root@sks.mirror.square-r00t.net:/srv/http/sks/dumps/.', + 'rsync': 'root@mirror.square-r00t.net:/srv/http/sks/dumps/.', # How many previous days of dumps should we keep? 'days': 1, # How many keys to include per dump file