i think... we're done. still some TODOs but seems to be in a workable state.
This commit is contained in:
parent
4dedd79942
commit
2ab99f0f22
@ -8,6 +8,7 @@ it should be a dict, structured like this:
|
||||
'keys': 'EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,admin@sysadministrivia.com',
|
||||
'keyservers': 'hkp://sks.mirror.square-r00t.net:11371,hkps://hkps.pool.sks-keyservers.net:443,http://pgp.mit.edu:80',
|
||||
'local': 'false',
|
||||
'msmtp_profile': None,
|
||||
'notify': True,
|
||||
'sigkey': '748231EBCBD808A14F5E85D28C004C2F93481F6B',
|
||||
'testkeyservers': False,
|
||||
@ -18,6 +19,8 @@ This *may* be reworked in the future to provide a mechanism for external calls t
|
||||
it's up to you to provide all the data in the dict in the above format.
|
||||
|
||||
It will then internally verify these items and do various conversions, so that self.args becomes this:
|
||||
(Note that some keys, such as "local", are validated and converted to appropriate values later on
|
||||
e.g. 'false' => False)
|
||||
|
||||
{'batch': False,
|
||||
'checklevel': None,
|
||||
@ -34,6 +37,7 @@ It will then internally verify these items and do various conversions, so that s
|
||||
'proto': 'http',
|
||||
'server': 'pgp.mit.edu'}],
|
||||
'local': 'false',
|
||||
'msmtp_profile': None,
|
||||
'notify': True,
|
||||
'rcpts': {'EFD9413B17293AFDFE6EA6F1402A088DEDF104CB': {'type': 'fpr'},
|
||||
'admin@sysadministrivia.com': {'type': 'email'}},
|
||||
|
@ -8,8 +8,8 @@ o = object
|
||||
|
||||
- pkey's dict key is the 40-char key ID of the primary key
|
||||
- "==>" indicates the next item is a dict and the current item may contain one or more elements of the same format,
|
||||
"++>" is a list,
|
||||
"-->" is a "flat" item (string, object, int, etc.)
|
||||
"++>" is a list,
|
||||
"-->" is a "flat" item (string, object, int, etc.)
|
||||
-"status" is one of "an UPGRADE", "a DOWNGRADE", or "a NEW TRUST".
|
||||
|
||||
keys(d) ==> (40-char key ID)(s) ==> pkey(d) --> email(s)
|
||||
@ -27,18 +27,101 @@ keys(d) ==> (40-char key ID)(s) ==> pkey(d) --> email(s)
|
||||
==> uids(d) ==> email(s) --> name(s)
|
||||
--> comment(s)
|
||||
--> email(s)
|
||||
--> updated(o, datetime)
|
||||
--> updated(o, datetime)*
|
||||
|
||||
* For many keys, this is unset. In-code, this is represented by having a timestamp of 0, or a
|
||||
datetime object matching UNIX epoch. This is converted to a string, "Never/unknown".
|
||||
|
||||
for email templates, they are looped over for each key dict as "key".
|
||||
so for example, instead of specifying "keys['748231EBCBD808A14F5E85D28C004C2F93481F6B']['pkey']['name']",
|
||||
you instead should specify "key['pkey']['name']". To get the name of e.g. the second uid,
|
||||
you'd use "key['uids'][(uid email)]['name'].
|
||||
|
||||
the same structure is available via the "mykey" dictionary. e.g. to get the key ID of *your* key,
|
||||
you can use "mykey['subkeys'][0][0]".
|
||||
e.g. in the code, it's this:
|
||||
{'748231EBCBD808A14F5E85D28C004C2F93481F6B': {'change': None,
|
||||
'check': 0,
|
||||
'local': False,
|
||||
'notify': True,
|
||||
'pkey': {'creation': '2013-12-10 '
|
||||
'08:35:52',
|
||||
'email': 'brent.saner@gmail.com',
|
||||
'key': '<GPGME object>',
|
||||
'name': 'Brent Timothy '
|
||||
'Saner'},
|
||||
'sign': True,
|
||||
'status': None,
|
||||
'subkeys': {'748231EBCBD808A14F5E85D28C004C2F93481F6B': '2013-12-10 '
|
||||
'08:35:52'},
|
||||
'trust': 2,
|
||||
'uids': {'brent.saner@gmail.com': {'comment': '',
|
||||
'name': 'Brent '
|
||||
'Timothy '
|
||||
'Saner',
|
||||
'updated': 'Never/unknown'},
|
||||
'bts@square-r00t.net': {'comment': 'http://www.square-r00t.net',
|
||||
'name': 'Brent '
|
||||
'S.',
|
||||
'updated': 'Never/unknown'},
|
||||
'r00t@sysadministrivia.com': {'comment': 'https://sysadministrivia.com',
|
||||
'name': 'r00t^2',
|
||||
'updated': 'Never/unknown'},
|
||||
'squarer00t@keybase.io': {'comment': '',
|
||||
'name': 'keybase.io/squarer00t',
|
||||
'updated': 'Never/unknown'}}}}
|
||||
but this is passed to the email template as:
|
||||
{'change': None,
|
||||
'check': 0,
|
||||
'local': False,
|
||||
'notify': True,
|
||||
'pkey': {'creation': '2013-12-10 08:35:52',
|
||||
'email': 'brent.saner@gmail.com',
|
||||
'key': '<GPGME object>',
|
||||
'name': 'Brent Timothy Saner'},
|
||||
'sign': True,
|
||||
'status': None,
|
||||
'subkeys': {'748231EBCBD808A14F5E85D28C004C2F93481F6B': '2013-12-10 08:35:52'},
|
||||
'trust': 2,
|
||||
'uids': {'brent.saner@gmail.com': {'comment': '',
|
||||
'name': 'Brent Timothy Saner',
|
||||
'updated': '1970-01-01 00:00:00'},
|
||||
'bts@square-r00t.net': {'comment': 'http://www.square-r00t.net',
|
||||
'name': 'Brent S.',
|
||||
'updated': 'Never/unknown'},
|
||||
'r00t@sysadministrivia.com': {'comment': 'https://sysadministrivia.com',
|
||||
'name': 'r00t^2',
|
||||
'updated': 'Never/unknown'},
|
||||
'squarer00t@keybase.io': {'comment': '',
|
||||
'name': 'keybase.io/squarer00t',
|
||||
'updated': 'Never/unknown'}}}
|
||||
|
||||
(because the emails are iterated through the keys).
|
||||
|
||||
|
||||
the same structure is available via the "mykey" dictionary (e.g. to get the key ID of *your* key,
|
||||
you can use "mykey['subkeys'][0][0]"):
|
||||
|
||||
{'change': False,
|
||||
'check': None,
|
||||
'local': False,
|
||||
'notify': False,
|
||||
'pkey': {'creation': '2017-09-07 20:54:31',
|
||||
'email': 'test@test.com',
|
||||
'key': '<GPGME object>',
|
||||
'name': 'test user'},
|
||||
'sign': False,
|
||||
'status': None,
|
||||
'subkeys': {'1CD9200637EC587D1F8EB94198748C2879CCE88D': '2017-09-07 20:54:31',
|
||||
'2805EC3D90E2229795AFB73FF85BC40E6E17F339': '2017-09-07 20:54:31'},
|
||||
'trust': 'ultimate',
|
||||
'uids': {'test@test.com': {'comment': 'this is a testing junk key. DO NOT '
|
||||
'IMPORT/SIGN/TRUST.',
|
||||
'name': 'test user',
|
||||
'updated': 'Never/unknown'}}}
|
||||
|
||||
|
||||
you also have the following variables/lists/etc. available for templates (via the Jinja2 templating syntax[0]):
|
||||
- "keyservers", a list of keyservers set.
|
||||
|
||||
|
||||
|
||||
[0] http://jinja.pocoo.org/docs/2.9/templates/
|
||||
|
257
gpg/kant/docs/kant.1
Normal file
257
gpg/kant/docs/kant.1
Normal file
@ -0,0 +1,257 @@
|
||||
'\" t
|
||||
.\" Title: kant
|
||||
.\" Author: Brent Saner
|
||||
.\" Generator: Asciidoctor 1.5.6.1
|
||||
.\" Date: 2017-09-21
|
||||
.\" Manual: KANT - Keysigning and Notification Tool
|
||||
.\" Source: KANT
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "KANT" "1" "2017-09-21" "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<KEY_IDS|BATCHFILE>\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.
|
||||
On successful completion, information about the keys that were signed and the key used to sign are saved to ~/.kant/cache/YYYY.MM.DD_HH.MM.SS.
|
||||
.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. See \fBMAIL\fP for more information.
|
||||
.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\-m\fP \fIPROFILE\fP, \fB\-\-msmtp\-profile\fP \fIPROFILE\fP
|
||||
.RS 4
|
||||
If specified, use the msmtp profile named \fIPROFILE\fP. If this is not specified, KANT first looks for an msmtp configuration named KANT (case\-sensitive). If it doesn\(cqt find one, it will use the profile specified as the default profile in your msmtp configuration. See \fBMAIL\fP for more information.
|
||||
.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/<yourusername>/.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 "MAIL"
|
||||
.sp
|
||||
The mailing feature of KANT is very handy; it will let you send notifications to the owners of the keys you sign. This is encouraged because: 1.) it\(cqs courteous to let them know where they can fetch the signature you just made on their key, 2.) it\(cqs courteous to let them know if you did/did not push to a keyserver (some people don\(cqt want their keys pushed, and it\(cqs a good idea to respect that wish), and 3.) the mailer also attaches the pubkey for the key you used to sign with, in case your key isn\(cqt on a keyserver, etc.
|
||||
.sp
|
||||
However, in order to do this since many ISPs block outgoing mail, one would typically use something like msmtp (http://msmtp.sourceforge.net/). Note that you don\(cqt even need msmtp to be installed, you just need to have msmtp configuration files set up via either /etc/msmtprc or ~/.msmtprc. KANT will parse these configuration files and use a purely pythonic implementation for sending the emails (see \fBSENDING\fP).
|
||||
.sp
|
||||
It supports templated mail messages as well (see \fBTEMPLATES\fP). It sends a MIME multipart email, in both plaintext and HTML formatting, for mail clients that may only support one or the other. It will also sign the email message using your signing key (see \fB\-K\fP, \fB\-\-sigkey\fP) and attach a binary (.gpg) and ASCII\-armored (.asc) export of your pubkey.
|
||||
.SS "SENDING"
|
||||
.sp
|
||||
KANT first looks for ~/.msmtprc and, if not found, will look for /etc/msmtprc. If neither are found, mail notifications will not be sent and it will be up to you to contact the key owner(s) and let them know you have signed their key(s). If it does find either, it will use the first configuration file it finds and first look for a profile called "KANT" (without quotation marks). If this is not found, it will use whatever profile is specified for as the default profile (e.g. \fBaccount default: someprofilename\fP in the msmtprc).
|
||||
.SS "TEMPLATES"
|
||||
.sp
|
||||
KANT, on first run (even with a \fB\-h\fP/\fB\-\-help\fP execution), will create the default email templates (which can be found as ~/.kant/email.html.j2 and ~/.kant/email.plain.j2). These support templating via Jinja2 (http://jinja.pocoo.org/docs/2.9/templates/), and the following variables/dictionaries/lists are exported for your use:
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
* \fBkey\fP \- a dictionary of information about the recipient\(aqs key (see docs/REF.keys.struct.txt)
|
||||
* \fBmykey\fP \- a dictionary of information about your key (see docs/REF.keys.struct.txt)
|
||||
* \fBkeyservers\fP \- a list of keyservers that the key has been pushed to (if an exportable/non\-local signature was made)
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
And of course you can set your own variables inside the template as well (http://jinja.pocoo.org/docs/2.9/templates/#assignments).
|
||||
.SH "SEE ALSO"
|
||||
.sp
|
||||
gpg(1), gpgconf(1), msmtp(1)
|
||||
.SH "RESOURCES"
|
||||
.sp
|
||||
\fBAuthor\(cqs web site:\fP https://square\-r00t.net/
|
||||
.sp
|
||||
\fBAuthor\(cqs GPG information:\fP 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
|
@ -20,6 +20,7 @@ Keysigning (and keysigning parties) can be a lot of fun, and can offer someone w
|
||||
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.
|
||||
On successful completion, information about the keys that were signed and the key used to sign are saved to ~/.kant/cache/YYYY.MM.DD_HH.MM.SS.
|
||||
|
||||
*-h*, *--help*::
|
||||
Display brief help/usage and exit.
|
||||
@ -47,11 +48,14 @@ set owner trust, specify level of checking done, and push the signatures to a ke
|
||||
*-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.
|
||||
but this flag is given, then we will NOT attempt to send emails. See *MAIL* for more information.
|
||||
|
||||
*-s* _KEYSERVER(S)_, *--keyservers* _KEYSERVER(S)_::
|
||||
The comma-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime.
|
||||
|
||||
*-m* _PROFILE_, *--msmtp-profile* _PROFILE_::
|
||||
If specified, use the msmtp profile named _PROFILE_. If this is not specified, KANT first looks for an msmtp configuration named KANT (case-sensitive). If it doesn't find one, it will use the profile specified as the default profile in your msmtp configuration. See *MAIL* for more information.
|
||||
|
||||
*-b*, *--batch*::
|
||||
If specified, operate in batch mode. See *BATCHFILE* for more information.
|
||||
|
||||
@ -153,13 +157,36 @@ the author follows):
|
||||
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]
|
||||
|
||||
== MAIL
|
||||
The mailing feature of KANT is very handy; it will let you send notifications to the owners of the keys you sign. This is encouraged because: 1.) it's courteous to let them know where they can fetch the signature you just made on their key, 2.) it's courteous to let them know if you did/did not push to a keyserver (some people don't want their keys pushed, and it's a good idea to respect that wish), and 3.) the mailer also attaches the pubkey for the key you used to sign with, in case your key isn't on a keyserver, etc.
|
||||
|
||||
However, in order to do this since many ISPs block outgoing mail, one would typically use something like msmtp (\http://msmtp.sourceforge.net/). Note that you don't even need msmtp to be installed, you just need to have msmtp configuration files set up via either /etc/msmtprc or ~/.msmtprc. KANT will parse these configuration files and use a purely pythonic implementation for sending the emails (see *SENDING*).
|
||||
|
||||
It supports templated mail messages as well (see *TEMPLATES*). It sends a MIME multipart email, in both plaintext and HTML formatting, for mail clients that may only support one or the other. It will also sign the email message using your signing key (see *-K*, *--sigkey*) and attach a binary (.gpg) and ASCII-armored (.asc) export of your pubkey.
|
||||
|
||||
=== SENDING
|
||||
KANT first looks for ~/.msmtprc and, if not found, will look for /etc/msmtprc. If neither are found, mail notifications will not be sent and it will be up to you to contact the key owner(s) and let them know you have signed their key(s). If it does find either, it will use the first configuration file it finds and first look for a profile called "KANT" (without quotation marks). If this is not found, it will use whatever profile is specified for as the default profile (e.g. *account default: someprofilename* in the msmtprc).
|
||||
|
||||
=== TEMPLATES
|
||||
KANT, on first run (even with a *-h*/*--help* execution), will create the default email templates (which can be found as ~/.kant/email.html.j2 and ~/.kant/email.plain.j2). These support templating via Jinja2 (\http://jinja.pocoo.org/docs/2.9/templates/), and the following variables/dictionaries/lists are exported for your use:
|
||||
|
||||
[subs=+quotes]
|
||||
....
|
||||
* *key* - a dictionary of information about the recipient's key (see docs/REF.keys.struct.txt)
|
||||
* *mykey* - a dictionary of information about your key (see docs/REF.keys.struct.txt)
|
||||
* *keyservers* - a list of keyservers that the key has been pushed to (if an exportable/non-local signature was made)
|
||||
....
|
||||
|
||||
And of course you can set your own variables inside the template as well (\http://jinja.pocoo.org/docs/2.9/templates/#assignments).
|
||||
|
||||
== SEE ALSO
|
||||
gpg(1), gpgconf(1)
|
||||
gpg(1), gpgconf(1), msmtp(1)
|
||||
|
||||
== RESOURCES
|
||||
|
||||
*Author's web site:* https://square-r00t.net/
|
||||
*Author's GPG information:* https://square-r00t.net/gpg-info
|
||||
*Author's web site:* \https://square-r00t.net/
|
||||
|
||||
*Author's GPG information:* \https://square-r00t.net/gpg-info
|
||||
|
||||
== COPYING
|
||||
|
||||
|
138
gpg/kant/kant.py
138
gpg/kant/kant.py
@ -32,35 +32,33 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
# These are the "stock" templates for emails. It's a PITA, but to save some space since we store them
|
||||
# inline in here, they're XZ'd and base64'd.
|
||||
self.email_tpl = {}
|
||||
self.email_tpl['plain'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AWYAs1dACQZSZhvFgKNdKNXbSf05z0ZPvTvmdQ0mJQg' +
|
||||
'atgzhPVeLKxz22bhxedC813X5I8Gn2g9q9Do2jPPViyuLf1SFHDUx1m7SEFsSUyT7L/j71tuxRZi' +
|
||||
'xLyy6P3mHo3HeUhwgpgh6lvMYwlTf+zhj3558RJmhXprLteoKt/sY4NIMzz3TBa0ivsVo6EFA9/G' +
|
||||
'2q1MpuHxg86uY1pA4tkFlmxuSklZq5EKuu7B5RSeUGB+SsjDPSfsdhoPngMET1EXTZfVSWezjJkH' +
|
||||
'REyn5SgqpD7vwmwvZWcwWua+V+e/rYYF1cx0Y0Wi1wAC6NzGDce3gbcr/k6M/PiMi8ekJPCmgMgP' +
|
||||
'R4GBtRi121wU374nml42WjdGjHee6Se6d0OGKscJjB5Z1eSOho63OMn5Ayu4Lvl05L/mB88Hnk5e' +
|
||||
'MayYmXqQdhx3ualWiD/TdHwLf+79t43wfjs6Z/aq/lku67SGdUpjuYRV52nls6WPTuBmo2oCbzpP' +
|
||||
'MojIXiYHR3cWebI2CdnVTTHHz7el4NAWIlPKtZkPR6VYj2DkND4kmkO92I258hUqLARWnNaywx87' +
|
||||
'hFzhaGN9oKZdozKSZpEDyZUsymWuhQnnY76EImeha67LJwSsXLpBxuViCNlqv7ATT9iffzDmjm0i' +
|
||||
'2MeLix3rBRMWp4MmWC8bP1ZAKOEq4M3hwjt1q68fH4QvtmTyic/7DBW2KsgZRu21RjK7tHtKTxwy' +
|
||||
'2UN3pGT6uZRL8vNRNvD70UaeD5MW7lFBPehIeFoByaEKGFfeS8dKc1VFauDmkRlhOboLkPqqwbV+' +
|
||||
'tUmU8UvTftKIx1RwTm3FKqjKCYrdqp0fL5wsA93YhRJqHfkvtjCjzRc0czszURkJMHfPyttQg2jb' +
|
||||
'UQVIg40XMgew2EUdCrJC3wTvXm9tBxMqXAFBn6S4ihqiJ5PTUDKCx7EnAjK3kUwbWvDno8h1M9u9' +
|
||||
'teC5CEWhcAAAAAAwG5UqO9bdswAB6QWZCwAAKFDj9rHEZ/sCAAAAAARZWg==')
|
||||
self.email_tpl['html'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AXiAt1dAB4aCobvStaHNqdVBn1LjZcL+G+98rmZ7eGZ' +
|
||||
self.email_tpl['plain'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4ATxAnZdACQZSZhvFgKNdKNXbSf05z0ZPvTvmdQ0mJQg' +
|
||||
'atgzhPVeLKxz22bhxedC813X5I8Gn2g9q9Do2jPPgXOzysImWXoraY4mhz0BAo2Zx1u6AiQQLdN9' +
|
||||
'/jwrDrUEtb8M/QzmRd+8JrYN8s8vhViJZARMNHYnPeQK5GYEoGZEQ8l2ULmpTjAn9edSnrMmNSb2' +
|
||||
'EC86CuyhaWDPsQeIamWW1t+MWmgsggE3xKYADKXHMQyXvhv/TAn987dEbzmrkpg8PCjxWt1wKRAr' +
|
||||
'siDpCGvXLiBwnDtN1D7ocwbZVKty2GELbYt0f0CT7n5Pyu9n0P7QMnErM38kLR1nReopQp41+CsG' +
|
||||
'orb8EpGGVdFa7sSWSANQtGTjx/1JHecpkTN8xX4kAjMWKYujWlZi/HzN7y/W5GDJM3ycVEUTsDRV' +
|
||||
'6AusncRBFbo4/+K6cn5WCrhqd5jY2vDJR6KcO0O3usHUMzvOF0S0CZhUbA3Mil5DmPwFrdFrESby' +
|
||||
'O1xH3uvgHpA5X91qkpEajokOOkY3FZm0oeANh9AMoMfDFTuqi41Nq9Myk4VKNEfzioChn9IfFxX0' +
|
||||
'Luw6OyXtWJdpe3BvO7pWazLhvdIY4poh9brvJ25cG1kDMOlmC3NEb+POeqQ5aUr4XaRqFstk3grb' +
|
||||
'8EjiGBzg18uHsbhjyReXnZprJjwzWUdwpV6j+2JFI13UEp16oTyTwyhHdpAmAg+lQJQxtcMpnUeX' +
|
||||
'/xBkQGs+rqe0e/i8ZQ80XsLAoScxUL+45v9vANYV+lCWRnm/2GZOtCFs1Cb4t9hOeV0P1cwxw7fG' +
|
||||
'b1A921JUkHbASFiv2EFsgf0lkvnMgz2slNXKcLuwB6X0CAAAALypR4JWDUR6AAGSBfIJAABGCaV4' +
|
||||
'scRn+wIAAAAABFla')
|
||||
self.email_tpl['html'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AXfAtVdAB4aCobvStaHNqdVBn1LjZcL+G+98rmZ7eGZ' +
|
||||
'Wqx+3LjENIv37L1aGPICZDRBrsBugzVSasRBHkdHMrWW7SsPfRzw6btQfASTHLr48auPJlJgXTnb' +
|
||||
'vgDd2ELrs6p5kXZwE7+pedhgffAcekwr0eyqVNzzdUWJpvZcDBGtp+yvIwSAcrKkUxUd5AFBigd3' +
|
||||
'4IW1XEK3eQ+LSSEIquUugrd3xiFB4rkSSDGbAuVLqy4Sq3w5c8RRAKavhfSn154/H/D+3RhqFHc/' +
|
||||
'/x51rgXFvgJ/DwYrr9g7JV9EQB15JvJJxazgnWftZE0W0u05Z7QNrIZQMG6LjcSjf0ep1zNYrJkf' +
|
||||
'UYOfHNjXGfpmIfG0Y/cNhU1Zqv3ohv6EGWk4B7CdLjXEeDYqkJgIGA6Q4FQXM6PF7blXFQJ3papF' +
|
||||
'lH0iO+ElK3LZVcql+QcVt2Ci+hwiKzsUvV5ydnHezyViTYTppjlZzju5SxxddQg+7CwGzX9O8ys1' +
|
||||
'8dlbMFHD2ruPd4Zig9B1TEKHSdmQQGwITNufbSGixbuOZAbfMXP1oQqSzYkbbx2ye8ddISOr/753' +
|
||||
'deLOwaQpy6tK9nb1wYwsfhpmZriWYDcKRfjkgr0srxnC2iDlMB0Do+GCLVVlmju9qcxeObWoxUaX' +
|
||||
'TqecRW4fbpa9xAIH0tZlOpIyPgGfm3CXkiGOs/J/QJ4C4spqNpwppoXg6EAig7Y9GStQyEsHXZrj' +
|
||||
'vLAefyaseybuMC+9okhx8VYM8esuE2GVTbCbWhn8ZTi8posQ+zabXvk7KE5zwGHDcvSGg0bYctJj' +
|
||||
'V6pExLCp1vCUdP3iP06OCFMINDnGR7ZP4Da/atBUuB/F0LN//x+HfwhEUpVTG52L7f6Qjd/LhvU2' +
|
||||
'f/zVfMKlw5xXwTjBu2X1oRYfhyYFhgnDECEi9iuRiVwwtnUU39r2XoaGcnMTPnZe62oy2jqTp3p+' +
|
||||
'Y+klB9jUwPUg2t5IxptZ0D/H5flD+pEAAAAAYczECM+Nfu0AAfkF4wsAAEsSt/GxxGf7AgAAAAAE' +
|
||||
'WVo=')
|
||||
'vgDd2ELrs6p5m5Wip3qD4NeNuwj4QMcxszWF1vLa1oZiNAmCSunIF8bNTw+lmI50h2M6bXfx80Og' +
|
||||
'T2HGcuTp07Mp+XLyZQJ5lbQyu5BRhwyKpu14sq9qrVkxmYt8AAxgUyhvRkooHSuug4O8ArMFXqqX' +
|
||||
'usX9P3zERAsi/TqWIFaG0xoBdrWf/zpGtsVQ+5TtCGOfUHGfIBaNy9Q+FOvfLJFYEzxac992Fkd0' +
|
||||
'as4RsN31FaySbBmZ8eB3zGbpjS7QH7CA70QYkRcYXcjWE9xHD3Wzxa3DFE0ihKAyVwakxvjgYa2B' +
|
||||
'7G6uYO606c+a6vHfPhgvY7Eph+I7ip0btfBbcKZ+XBSd0DtCd7ZvI7vlGJdW2/OBXHfNmCndMP1W' +
|
||||
'Ujd0ASQAQBbJr4rIxYygckSPWti4nBe9JpKTVWqdWRXWjeYGci1dKIjKs7JfS1PGJR50iuyANBun' +
|
||||
'yQ9oIRafb3nreBqtpXZ4LKM5hC697BaeOIcocXyMALf0a06AUmIaRQfO3AZrPxyOPH3EYOKIMrjM' +
|
||||
'EosihPVVyYuKUVOg3wWq5aeIC9zM7Htw4FNh2NB5QDYY6HxIqIVUfHCGz+4GaPBVaf0eie8kHaQR' +
|
||||
'xj+DkAiWQDmN/JRZeTlsy4d3P8XcArOLmxzql/iDzFqtzpD5d91o8I3HU9BJlDJFPs8bC2eCjYs8' +
|
||||
'o3WJET/UIch6YXQOemXa72aWdBVSytfKBMtL7uekd4ARGbFZYyW2x1agkAZGiWt7gwY8RVEoKyZH' +
|
||||
'bbvIvOhQ/j1BDuJFJO3BEgekeLhBPpG7cEewseXjGjoWZWtGr+qFTI//w+oDtdqGtJaGtELL3WYU' +
|
||||
'/tMiQU9AfXkTsODAjvduAAAAAIixVQ23iBDFAAHxBeALAADIP1EPscRn+wIAAAAABFla')
|
||||
# Set up a dict of some constants and mappings
|
||||
self.maps = {}
|
||||
# Keylist modes
|
||||
@ -130,7 +128,7 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
self.getTpls() # Build out self.tpls
|
||||
return(None)
|
||||
|
||||
def getEditPrompt(self, key): # "key" should be the FPR of the primary key
|
||||
def getEditPrompt(self, key, cmd): # "key" should be the FPR of the primary key
|
||||
# This mapping defines the default "answers" to the gpgme key editing.
|
||||
# https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
|
||||
# https://searchcode.com/codesearch/view/20535820/
|
||||
@ -161,7 +159,7 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
'trustsig_prompt': {'trust_value': str(_loctrust), # This requires some processing; see above
|
||||
'trust_depth': str(_locdepth), # The "depth" of the trust signature.
|
||||
'trust_regexp': None}, # We can "Restrict" trust to certain domains, but this isn't really necessary.
|
||||
'keyedit': {'prompt': 'trust', # Initiate trust editing
|
||||
'keyedit': {'prompt': cmd, # Initiate trust editing
|
||||
'save': {'okay': 'yes'}}}} # Save if prompted
|
||||
return(_map)
|
||||
|
||||
@ -271,21 +269,25 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
_keys[k.fpr]['uids'][s.email] = {'comment': s.comment,
|
||||
'updated': s.last_update}
|
||||
if len(_keytmp) > 1: # Print the keys and prompt for a selection.
|
||||
print('\nWe found the following keys for {0} <{1}>...\n\nKEY ID:'.format(r, _orig_r))
|
||||
for s in _keys[r]:
|
||||
print(s, _keys[r])
|
||||
print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(k,
|
||||
|
||||
print('\nWe found the following keys for {0}...\n\nKEY ID:'.format(r))
|
||||
for s in _keytmp:
|
||||
print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(s.fpr,
|
||||
'',
|
||||
datetime.datetime.utcfromtimestamp(s['updated'])))
|
||||
for email in _keys[k]['uids']:
|
||||
print('{0:42}(Generated {3}) <{2}> {1}'.format('',
|
||||
s['uids'][email]['comment'],
|
||||
email,
|
||||
datetime.datetime.utcfromtimestamp(s['uids'][email]['updated'])))
|
||||
datetime.datetime.utcfromtimestamp(s.subkeys[0].timestamp)))
|
||||
for u in s.uids:
|
||||
if u.last_update == 0:
|
||||
_updated = 'Never/Unknown'
|
||||
else:
|
||||
_updated = datetime.datetime.utcfromtimestamp(u.last_update)
|
||||
print('{0:42}(Updated {3}) <{2}> {1}'.format('',
|
||||
u.comment,
|
||||
u.email,
|
||||
_updated))
|
||||
print()
|
||||
while True:
|
||||
key = input('Please enter the (full) appropriate key: ')
|
||||
if key not in keys.keys():
|
||||
if key not in _keys.keys():
|
||||
print('Please enter a full key ID from the list above or hit ctrl-d to exit.')
|
||||
else:
|
||||
_keyids.append(key)
|
||||
@ -295,17 +297,21 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
print('Could not find {0}!'.format(r))
|
||||
del(self.args['rcpts'][r])
|
||||
continue
|
||||
_keyids.append(_keys[k.fpr]['fpr'])
|
||||
_keyids.append(k.fpr)
|
||||
print('\nFound key {0} for {1} (Generated at {2}):'.format(_keys[k.fpr]['fpr'],
|
||||
r,
|
||||
datetime.datetime.utcfromtimestamp(_keys[k.fpr]['created'])))
|
||||
for email in _keys[k.fpr]['uids']:
|
||||
if _keys[k.fpr]['uids'][email]['updated'] == 0:
|
||||
_updated = 'Never/Unknown'
|
||||
else:
|
||||
_updated = datetime.datetime.utcfromtimestamp(_keys[k.fpr]['uids'][email]['updated'])
|
||||
print('\t(Generated {2}) {0} <{1}>'.format(_keys[k.fpr]['uids'][email]['comment'],
|
||||
email,
|
||||
datetime.datetime.utcfromtimestamp(_keys[k.fpr]['uids'][email]['updated'])))
|
||||
email,
|
||||
_updated))
|
||||
print()
|
||||
## And now we can (FINALLY) fetch the key(s).
|
||||
# TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms?
|
||||
print(_keyids)
|
||||
for g in _keyids:
|
||||
try:
|
||||
self.ctx.op_import_keys([_keys[g]['obj']])
|
||||
@ -313,6 +319,8 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
print('Key {0} could not be found on the keyserver'.format(g)) # The key isn't on the keyserver
|
||||
self.ctx.set_keylist_mode(self.maps['keylist']['local'])
|
||||
for k in _keys:
|
||||
if k not in _keyids:
|
||||
continue
|
||||
_key = _keys[k]['obj']
|
||||
self.keys[k] = {'pkey': {'email': _key.uids[0].email,
|
||||
'name': _key.uids[0].name,
|
||||
@ -417,7 +425,7 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
'(Default is Unknown.)\n' +
|
||||
'\n\t\033[1m0 = Unknown\n\t1 = None\n\t2 = Casual\n\t3 = Careful\033[0m\nCheck level: ').format(k))
|
||||
if check_in == '':
|
||||
check_in == 'unknown'
|
||||
check_in = 'unknown'
|
||||
self.keys[k]['check'] = check_in
|
||||
return()
|
||||
|
||||
@ -469,6 +477,13 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
print('{0}: "{1}" is not a valid {2}; skipping. Run kant again to fix.'.format(k, self.keys[k][s], _errs[s]))
|
||||
del(self.keys[k])
|
||||
return()
|
||||
# Determine if we need to change the trust.
|
||||
if t == 'trust':
|
||||
cur_trust = self.keys[k]['pkey']['key'].owner_trust
|
||||
if cur_trust == self.keys[k]['trust']:
|
||||
self.keys[k]['change'] = False
|
||||
else:
|
||||
self.keys[k]['change'] = True
|
||||
return()
|
||||
|
||||
def sigKeys(self): # The More Business-End(TM)
|
||||
@ -521,10 +536,11 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
# TODO: add check for change
|
||||
for k in list(self.keys.keys()):
|
||||
_key = self.keys[k]
|
||||
_map = self.getEditPrompt(_key)
|
||||
out = gpg.Data()
|
||||
self.ctx.interact(_key['pkey']['key'], self.KeyEditor(_map).editKey, sink = out, fnc_value = out)
|
||||
out.seek(0, 0)
|
||||
if _key['change']:
|
||||
_map = self.getEditPrompt(_key, 'trust')
|
||||
out = gpg.Data()
|
||||
self.ctx.interact(_key['pkey']['key'], self.KeyEditor(_map).editKey, sink = out, fnc_value = out)
|
||||
out.seek(0, 0)
|
||||
return()
|
||||
|
||||
def pushKeys(self): # The Last Business-End(TM)
|
||||
@ -537,7 +553,7 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
def __init__(self):
|
||||
_homeconf = os.path.join(os.environ['HOME'], '.msmtprc')
|
||||
_sysconf = '/etc/msmtprc'
|
||||
self.msmtp = {'path': None}
|
||||
self.msmtp = {'conf': None}
|
||||
if not os.path.isfile(_homeconf):
|
||||
if not os.path.isfile(_sysconf):
|
||||
self.msmtp['conf'] = False
|
||||
@ -545,14 +561,10 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
self.msmtp['conf'] = _sysconf
|
||||
else:
|
||||
self.msmtp['conf'] = _homeconf
|
||||
if os.path.isfile(self.msmtp['conf']):
|
||||
for p in (os.environ['PATH']).split(':'):
|
||||
if os.path.isfile(os.path.join(p, 'msmtp')):
|
||||
self.msmtp['path'] = os.path.join(p, 'msmtp')
|
||||
if self.msmtp['path']:
|
||||
# Okay. So we have a config file, which we're assuming to be set up correctly, and a path to a binary.
|
||||
# Now we need to parse the config.
|
||||
self.msmtp['cfg'] = self.getCfg()
|
||||
if self.msmtp['conf']:
|
||||
# Okay. So we have a config file, which we're assuming to be set up correctly.
|
||||
# Now we need to parse the config.
|
||||
self.msmtp['cfg'] = self.getCfg()
|
||||
return(None)
|
||||
|
||||
def getCfg(self):
|
||||
@ -753,7 +765,8 @@ class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||
else:
|
||||
r = k.replace('<', '').replace('>', '')
|
||||
locargs['rcpts'][r] = locargs['rcpts'][k]
|
||||
del(locargs['rcpts'][k])
|
||||
if k != r:
|
||||
del(locargs['rcpts'][k])
|
||||
k = r
|
||||
_ktype = 'email'
|
||||
locargs['rcpts'][k]['type'] = _ktype
|
||||
@ -901,6 +914,11 @@ def parseArgs():
|
||||
'Default keyserver list is: \n\n\t\033[1m{0}\033[0m\n\n'.format(re.sub(',',
|
||||
'\n\t',
|
||||
defkeyservers)))
|
||||
args.add_argument('-m',
|
||||
'--msmtp',
|
||||
dest = 'msmtp_profile',
|
||||
default = None,
|
||||
help = 'The msmtp profile to use to send the notification emails. See the man page for more information.')
|
||||
args.add_argument('-b',
|
||||
'--batch',
|
||||
dest = 'batch',
|
||||
|
Loading…
Reference in New Issue
Block a user