docs and examples: python bindings howto
authorBen McGinnes <ben@adversary.org>
Sun, 23 Sep 2018 09:36:54 +0000 (19:36 +1000)
committerBen McGinnes <ben@adversary.org>
Sun, 23 Sep 2018 09:36:54 +0000 (19:36 +1000)
* Added more comprehensive examples using hkp4py and added a couple
  more example scripts for protonmail.

Tested-by: Ben McGinnes <ben@adversary.org>
Signed-off-by: Ben McGinnes <ben@adversary.org>
doc/gpgme-python-howto.texi
lang/python/docs/gpgme-python-howto.org
lang/python/examples/howto/pmkey-import-hkp-alt.py [new file with mode: 0755]
lang/python/examples/howto/pmkey-import-hkp.py [new file with mode: 0755]

index 7b8b79d..2e30597 100644 (file)
@@ -86,6 +86,12 @@ Key selection
 
 * Counting keys::
 
+Importing keys
+
+* Working with ProtonMail::
+* Importing with HKP for Python::
+* Importing from ProtonMail with HKP for Python::
+
 Exporting keys
 
 * Exporting public keys::
@@ -832,7 +838,125 @@ relative ease by which such key IDs can be reproduced, as demonstrated
 by the Evil32 Project in 2014 (which was subsequently exploited in
 2016).
 
-Performing the same task with the @uref{https://github.com/Selfnet/hkp4py, hkp4py module} (available via PyPI)
+@menu
+* Working with ProtonMail::
+* Importing with HKP for Python::
+* Importing from ProtonMail with HKP for Python::
+@end menu
+
+@node Working with ProtonMail
+@subsection Working with ProtonMail
+
+Here is a variation on the example above which checks the constrained
+ProtonMail keyserver for ProtonMail public keys.
+
+@example
+import gpg
+import requests
+import sys
+
+print("""
+This script searches the ProtonMail key server for the specified key and
+imports it.
+""")
+
+c = gpg.Context(armor=True)
+url = "https://api.protonmail.ch/pks/lookup"
+ksearch = []
+
+if len(sys.argv) >= 2:
+    keyterm = sys.argv[1]
+else:
+    keyterm = input("Enter the key ID, UID or search string: ")
+
+if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True:
+    ksearch.append(keyterm[1:])
+    ksearch.append(keyterm[1:])
+    ksearch.append(keyterm[1:])
+elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True:
+    ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:]))
+    ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:]))
+    ksearch.append("@{0@}@@pm.me".format(keyterm[1:]))
+elif keyterm.count("@@") == 0:
+    ksearch.append("@{0@}@@protonmail.com".format(keyterm))
+    ksearch.append("@{0@}@@protonmail.ch".format(keyterm))
+    ksearch.append("@{0@}@@pm.me".format(keyterm))
+elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False:
+    uidlist = keyterm.split("@@")
+    for uid in uidlist:
+        ksearch.append("@{0@}@@protonmail.com".format(uid))
+        ksearch.append("@{0@}@@protonmail.ch".format(uid))
+        ksearch.append("@{0@}@@pm.me".format(uid))
+elif keyterm.count("@@") > 2:
+    uidlist = keyterm.split("@@")
+    for uid in uidlist:
+        ksearch.append("@{0@}@@protonmail.com".format(uid))
+        ksearch.append("@{0@}@@protonmail.ch".format(uid))
+        ksearch.append("@{0@}@@pm.me".format(uid))
+else:
+    ksearch.append(keyterm)
+
+for k in ksearch:
+    payload = @{"op": "get", "search": k@}
+    try:
+        r = requests.get(url, verify=True, params=payload)
+        if r.ok is True:
+            result = c.key_import(r.content)
+        elif r.ok is False:
+            result = r.content
+    except Exception as e:
+        result = None
+
+    if result is not None and hasattr(result, "considered") is False:
+        print("@{0@} for @{1@}".format(result.decode(), k))
+    elif result is not None and hasattr(result, "considered") is True:
+        num_keys = len(result.imports)
+        new_revs = result.new_revocations
+        new_sigs = result.new_signatures
+        new_subs = result.new_sub_keys
+        new_uids = result.new_user_ids
+        new_scrt = result.secret_imported
+        nochange = result.unchanged
+        print("""
+The total number of keys considered for import was:  @{0@}
+
+With UIDs wholely or partially matching the following string:
+
+        @{1@}
+
+   Number of keys revoked:  @{2@}
+ Number of new signatures:  @{3@}
+    Number of new subkeys:  @{4@}
+   Number of new user IDs:  @{5@}
+Number of new secret keys:  @{6@}
+ Number of unchanged keys:  @{7@}
+
+The key IDs for all considered keys were:
+""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt,
+           nochange))
+        for i in range(num_keys):
+            print(result.imports[i].fpr)
+        print("")
+    elif result is None:
+        print(e)
+@end example
+
+Both the above example, @uref{../examples/howto/pmkey-import.py, pmkey-import.py}, and a version which prompts
+for an alternative GnuPG home directory, @uref{../examples/howto/pmkey-import-alt.py, pmkey-import-alt.py}, are
+available with the other examples and are executable scripts.
+
+Note that while the ProtonMail servers are based on the SKS servers,
+their server is related more to their API and is not feature complete
+by comparison to the servers in the SKS pool.  One notable difference
+being that the ProtonMail server does not permit non ProtonMail users
+to update their own keys, which could be a vector for attacking
+ProtonMail users who may not receive a key's revocation if it had been
+compromised.
+
+@node Importing with HKP for Python
+@subsection Importing with HKP for Python
+
+Performing the same tasks with the @uref{https://github.com/Selfnet/hkp4py, hkp4py module} (available via PyPI)
 is not too much different, but does provide a number of options of
 benefit to end users.  Not least of which being the ability to perform
 some checks on a key before importing it or not.  For instance it may
@@ -905,69 +1029,278 @@ The key IDs for all considered keys were:
 @end example
 
 Since the hkp4py module handles multiple keys just as effectively as
-one (@samp{keys} is a list of responses per matching key), thie above
-example is able to do a little bit more with the returned data.
+one (@samp{keys} is a list of responses per matching key), the example
+above is able to do a little bit more with the returned data before
+anything is actually imported.
+
+@node Importing from ProtonMail with HKP for Python
+@subsection Importing from ProtonMail with HKP for Python
+
+Though this can provide certain benefits even when working with
+ProtonMail, the scope is somewhat constrained there due to the
+limitations of the ProtonMail keyserver.
+
+For instance, searching the SKS keyserver pool for the term "gnupg"
+produces hundreds of results from any time the word appears in any
+part of a user ID.  Performing the same search on the ProtonMail
+keyserver returns zero results, even though there are at least two
+test accounts which include it as part of the username.
+
+The cause of this discrepancy is the deliberate configuration of that
+server by ProtonMail to require an exact match of the full email
+address of the ProtonMail user whose key is being requested.
+Presumably this is intended to reduce breaches of privacy of their
+users as an email address must already be known before a key for that
+address can be obtained.
 
-Here is a variation on the first example above which checks the
-constrained ProtonMail keyserver for ProtonMail public keys.
+@enumerate
+@item
+Import from ProtonMail via HKP for Python Example no. 1
+
+
+The following script is avalable with the rest of the examples under
+the somewhat less than original name, @samp{pmkey-import-hkp.py}.
 
 @example
 import gpg
-import requests
+import hkp4py
+import os.path
 import sys
 
 print("""
 This script searches the ProtonMail key server for the specified key and
 imports it.
+
+Usage:  pmkey-import-hkp.py [search strings]
 """)
 
 c = gpg.Context(armor=True)
-url = "https://api.protonmail.ch/pks/lookup"
+server = hkp4py.KeyServer("hkps://api.protonmail.ch")
+keyterms = []
 ksearch = []
+allkeys = []
+results = []
+paradox = []
+homeless = None
 
-if len(sys.argv) >= 2:
+if len(sys.argv) > 2:
+    keyterms = sys.argv[1:]
+elif len(sys.argv) == 2:
     keyterm = sys.argv[1]
+    keyterms.append(keyterm)
+else:
+    key_term = input("Enter the key ID, UID or search string: ")
+    keyterms = key_term.split()
+
+for keyterm in keyterms:
+    if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True:
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+    elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True:
+        ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:]))
+        ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:]))
+        ksearch.append("@{0@}@@pm.me".format(keyterm[1:]))
+    elif keyterm.count("@@") == 0:
+        ksearch.append("@{0@}@@protonmail.com".format(keyterm))
+        ksearch.append("@{0@}@@protonmail.ch".format(keyterm))
+        ksearch.append("@{0@}@@pm.me".format(keyterm))
+    elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False:
+        uidlist = keyterm.split("@@")
+        for uid in uidlist:
+            ksearch.append("@{0@}@@protonmail.com".format(uid))
+            ksearch.append("@{0@}@@protonmail.ch".format(uid))
+            ksearch.append("@{0@}@@pm.me".format(uid))
+    elif keyterm.count("@@") > 2:
+        uidlist = keyterm.split("@@")
+        for uid in uidlist:
+            ksearch.append("@{0@}@@protonmail.com".format(uid))
+            ksearch.append("@{0@}@@protonmail.ch".format(uid))
+            ksearch.append("@{0@}@@pm.me".format(uid))
+    else:
+        ksearch.append(keyterm)
+
+for k in ksearch:
+    print("Checking for key for: @{0@}".format(k))
+    try:
+        keys = server.search(k)
+        if isinstance(keys, list) is True:
+            for key in keys:
+                allkeys.append(key)
+                try:
+                    import_result = c.key_import(key.key_blob)
+                except Exception as e:
+                    import_result = c.key_import(key.key)
+        else:
+            paradox.append(keys)
+            import_result = None
+    except Exception as e:
+        import_result = None
+    results.append(import_result)
+
+for result in results:
+    if result is not None and hasattr(result, "considered") is False:
+        print("@{0@} for @{1@}".format(result.decode(), k))
+    elif result is not None and hasattr(result, "considered") is True:
+        num_keys = len(result.imports)
+        new_revs = result.new_revocations
+        new_sigs = result.new_signatures
+        new_subs = result.new_sub_keys
+        new_uids = result.new_user_ids
+        new_scrt = result.secret_imported
+        nochange = result.unchanged
+        print("""
+The total number of keys considered for import was:  @{0@}
+
+With UIDs wholely or partially matching the following string:
+
+        @{1@}
+
+   Number of keys revoked:  @{2@}
+ Number of new signatures:  @{3@}
+    Number of new subkeys:  @{4@}
+   Number of new user IDs:  @{5@}
+Number of new secret keys:  @{6@}
+ Number of unchanged keys:  @{7@}
+
+The key IDs for all considered keys were:
+""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt,
+           nochange))
+        for i in range(num_keys):
+            print(result.imports[i].fpr)
+        print("")
+    elif result is None:
+        pass
+@end example
+
+@item
+Import from ProtonMail via HKP for Python Example no. 2
+
+
+Like its counterpart above, this script can also be found with the
+rest of the examples, by the name pmkey-import-hkp-alt.py.
+
+With this script a modicum of effort has been made to treat anything
+passed as a @samp{homedir} which either does not exist or which is not a
+directory, as also being a pssible user ID to check for.  It's not
+guaranteed to pick up on all such cases, but it should cover most of
+them.
+
+@example
+import gpg
+import hkp4py
+import os.path
+import sys
+
+print("""
+This script searches the ProtonMail key server for the specified key and
+imports it.  Optionally enables specifying a different GnuPG home directory.
+
+Usage:  pmkey-import-hkp.py [homedir] [search string]
+   or:  pmkey-import-hkp.py [search string]
+""")
+
+c = gpg.Context(armor=True)
+server = hkp4py.KeyServer("hkps://api.protonmail.ch")
+keyterms = []
+ksearch = []
+allkeys = []
+results = []
+paradox = []
+homeless = None
+
+if len(sys.argv) > 3:
+    homedir = sys.argv[1]
+    keyterms = sys.argv[2:]
+elif len(sys.argv) == 3:
+    homedir = sys.argv[1]
+    keyterm = sys.argv[2]
+    keyterms.append(keyterm)
+elif len(sys.argv) == 2:
+    homedir = ""
+    keyterm = sys.argv[1]
+    keyterms.append(keyterm)
 else:
     keyterm = input("Enter the key ID, UID or search string: ")
+    homedir = input("Enter the GPG configuration directory path (optional): ")
+    keyterms.append(keyterm)
 
-if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True:
-    ksearch.append(keyterm[1:])
-    ksearch.append(keyterm[1:])
-    ksearch.append(keyterm[1:])
-elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True:
-    ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:]))
-    ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:]))
-    ksearch.append("@{0@}@@pm.me".format(keyterm[1:]))
-elif keyterm.count("@@") == 0:
-    ksearch.append("@{0@}@@protonmail.com".format(keyterm))
-    ksearch.append("@{0@}@@protonmail.ch".format(keyterm))
-    ksearch.append("@{0@}@@pm.me".format(keyterm))
-elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False:
-    uidlist = keyterm.split("@@")
-    for uid in uidlist:
-        ksearch.append("@{0@}@@protonmail.com".format(uid))
-        ksearch.append("@{0@}@@protonmail.ch".format(uid))
-        ksearch.append("@{0@}@@pm.me".format(uid))
-elif keyterm.count("@@") > 2:
-    uidlist = keyterm.split("@@")
-    for uid in uidlist:
-        ksearch.append("@{0@}@@protonmail.com".format(uid))
-        ksearch.append("@{0@}@@protonmail.ch".format(uid))
-        ksearch.append("@{0@}@@pm.me".format(uid))
+if len(homedir) == 0:
+    homedir = None
+    homeless = False
+
+if homedir is not None:
+    if homedir.startswith("~"):
+        if os.path.exists(os.path.expanduser(homedir)) is True:
+            if os.path.isdir(os.path.expanduser(homedir)) is True:
+                c.home_dir = os.path.realpath(os.path.expanduser(homedir))
+            else:
+                homeless = True
+        else:
+            homeless = True
+    elif os.path.exists(os.path.realpath(homedir)) is True:
+        if os.path.isdir(os.path.realpath(homedir)) is True:
+            c.home_dir = os.path.realpath(homedir)
+        else:
+            homeless = True
+    else:
+        homeless = True
+
+# First check to see if the homedir really is a homedir and if not, treat it as
+# a search string.
+if homeless is True:
+    keyterms.append(homedir)
+    c.home_dir = None
 else:
-    ksearch.append(keyterm)
+    pass
+
+for keyterm in keyterms:
+    if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True:
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+    elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True:
+        ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:]))
+        ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:]))
+        ksearch.append("@{0@}@@pm.me".format(keyterm[1:]))
+    elif keyterm.count("@@") == 0:
+        ksearch.append("@{0@}@@protonmail.com".format(keyterm))
+        ksearch.append("@{0@}@@protonmail.ch".format(keyterm))
+        ksearch.append("@{0@}@@pm.me".format(keyterm))
+    elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False:
+        uidlist = keyterm.split("@@")
+        for uid in uidlist:
+            ksearch.append("@{0@}@@protonmail.com".format(uid))
+            ksearch.append("@{0@}@@protonmail.ch".format(uid))
+            ksearch.append("@{0@}@@pm.me".format(uid))
+    elif keyterm.count("@@") > 2:
+        uidlist = keyterm.split("@@")
+        for uid in uidlist:
+            ksearch.append("@{0@}@@protonmail.com".format(uid))
+            ksearch.append("@{0@}@@protonmail.ch".format(uid))
+            ksearch.append("@{0@}@@pm.me".format(uid))
+    else:
+        ksearch.append(keyterm)
 
 for k in ksearch:
-    payload = @{"op": "get", "search": k@}
+    print("Checking for key for: @{0@}".format(k))
     try:
-        r = requests.get(url, verify=True, params=payload)
-        if r.ok is True:
-            result = c.key_import(r.content)
-        elif r.ok is False:
-            result = r.content
+        keys = server.search(k)
+        if isinstance(keys, list) is True:
+            for key in keys:
+                allkeys.append(key)
+                try:
+                    import_result = c.key_import(key.key_blob)
+                except Exception as e:
+                    import_result = c.key_import(key.key)
+        else:
+            paradox.append(keys)
+            import_result = None
     except Exception as e:
-        result = None
+        import_result = None
+    results.append(import_result)
 
+for result in results:
     if result is not None and hasattr(result, "considered") is False:
         print("@{0@} for @{1@}".format(result.decode(), k))
     elif result is not None and hasattr(result, "considered") is True:
@@ -999,20 +1332,9 @@ The key IDs for all considered keys were:
             print(result.imports[i].fpr)
         print("")
     elif result is None:
-        print(e)
+        pass
 @end example
-
-Both the above example, @uref{../examples/howto/pmkey-import.py, pmkey-import.py}, and a version which prompts
-for an alternative GnuPG home directory, @uref{../examples/howto/pmkey-import-alt.py, pmkey-import-alt.py}, are
-available with the other examples and are executable scripts.
-
-Note that while the ProtonMail servers are based on the SKS servers,
-their server is related more to their API and is not feature complete
-by comparison to the servers in the SKS pool.  One notable difference
-being that the ProtonMail server does not permit non ProtonMail users
-to update their own keys, which could be a vector for attacking
-ProtonMail users who may not receive a key's revocation if it had been
-compromised.
+@end enumerate
 
 @node Exporting keys
 @section Exporting keys
index b5b9ed4..2f6ce73 100644 (file)
@@ -707,7 +707,125 @@ relative ease by which such key IDs can be reproduced, as demonstrated
 by the Evil32 Project in 2014 (which was subsequently exploited in
 2016).
 
-Performing the same task with the [[https://github.com/Selfnet/hkp4py][hkp4py module]] (available via PyPI)
+
+*** Working with ProtonMail
+    :PROPERTIES:
+    :CUSTOM_ID: import-protonmail
+    :END:
+
+Here is a variation on the example above which checks the constrained
+ProtonMail keyserver for ProtonMail public keys.
+
+#+BEGIN_SRC python -i
+import gpg
+import requests
+import sys
+
+print("""
+This script searches the ProtonMail key server for the specified key and
+imports it.
+""")
+
+c = gpg.Context(armor=True)
+url = "https://api.protonmail.ch/pks/lookup"
+ksearch = []
+
+if len(sys.argv) >= 2:
+    keyterm = sys.argv[1]
+else:
+    keyterm = input("Enter the key ID, UID or search string: ")
+
+if keyterm.count("@") == 2 and keyterm.startswith("@") is True:
+    ksearch.append(keyterm[1:])
+    ksearch.append(keyterm[1:])
+    ksearch.append(keyterm[1:])
+elif keyterm.count("@") == 1 and keyterm.startswith("@") is True:
+    ksearch.append("{0}@protonmail.com".format(keyterm[1:]))
+    ksearch.append("{0}@protonmail.ch".format(keyterm[1:]))
+    ksearch.append("{0}@pm.me".format(keyterm[1:]))
+elif keyterm.count("@") == 0:
+    ksearch.append("{0}@protonmail.com".format(keyterm))
+    ksearch.append("{0}@protonmail.ch".format(keyterm))
+    ksearch.append("{0}@pm.me".format(keyterm))
+elif keyterm.count("@") == 2 and keyterm.startswith("@") is False:
+    uidlist = keyterm.split("@")
+    for uid in uidlist:
+        ksearch.append("{0}@protonmail.com".format(uid))
+        ksearch.append("{0}@protonmail.ch".format(uid))
+        ksearch.append("{0}@pm.me".format(uid))
+elif keyterm.count("@") > 2:
+    uidlist = keyterm.split("@")
+    for uid in uidlist:
+        ksearch.append("{0}@protonmail.com".format(uid))
+        ksearch.append("{0}@protonmail.ch".format(uid))
+        ksearch.append("{0}@pm.me".format(uid))
+else:
+    ksearch.append(keyterm)
+
+for k in ksearch:
+    payload = {"op": "get", "search": k}
+    try:
+        r = requests.get(url, verify=True, params=payload)
+        if r.ok is True:
+            result = c.key_import(r.content)
+        elif r.ok is False:
+            result = r.content
+    except Exception as e:
+        result = None
+
+    if result is not None and hasattr(result, "considered") is False:
+        print("{0} for {1}".format(result.decode(), k))
+    elif result is not None and hasattr(result, "considered") is True:
+        num_keys = len(result.imports)
+        new_revs = result.new_revocations
+        new_sigs = result.new_signatures
+        new_subs = result.new_sub_keys
+        new_uids = result.new_user_ids
+        new_scrt = result.secret_imported
+        nochange = result.unchanged
+        print("""
+The total number of keys considered for import was:  {0}
+
+With UIDs wholely or partially matching the following string:
+
+        {1}
+
+   Number of keys revoked:  {2}
+ Number of new signatures:  {3}
+    Number of new subkeys:  {4}
+   Number of new user IDs:  {5}
+Number of new secret keys:  {6}
+ Number of unchanged keys:  {7}
+
+The key IDs for all considered keys were:
+""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt,
+           nochange))
+        for i in range(num_keys):
+            print(result.imports[i].fpr)
+        print("")
+    elif result is None:
+        print(e)
+#+END_SRC
+
+Both the above example, [[../examples/howto/pmkey-import.py][pmkey-import.py]], and a version which prompts
+for an alternative GnuPG home directory, [[../examples/howto/pmkey-import-alt.py][pmkey-import-alt.py]], are
+available with the other examples and are executable scripts.
+
+Note that while the ProtonMail servers are based on the SKS servers,
+their server is related more to their API and is not feature complete
+by comparison to the servers in the SKS pool.  One notable difference
+being that the ProtonMail server does not permit non ProtonMail users
+to update their own keys, which could be a vector for attacking
+ProtonMail users who may not receive a key's revocation if it had been
+compromised.
+
+
+*** Importing with HKP for Python
+    :PROPERTIES:
+    :CUSTOM_ID: import-hkp4py
+    :END:
+
+Performing the same tasks with the [[https://github.com/Selfnet/hkp4py][hkp4py module]] (available via PyPI)
 is not too much different, but does provide a number of options of
 benefit to end users.  Not least of which being the ability to perform
 some checks on a key before importing it or not.  For instance it may
@@ -780,69 +898,284 @@ The key IDs for all considered keys were:
 #+END_SRC
 
 Since the hkp4py module handles multiple keys just as effectively as
-one (=keys= is a list of responses per matching key), thie above
-example is able to do a little bit more with the returned data.
+one (=keys= is a list of responses per matching key), the example
+above is able to do a little bit more with the returned data before
+anything is actually imported.
+
+
+*** Importing from ProtonMail with HKP for Python
+    :PROPERTIES:
+    :CUSTOM_ID: import-protonmail-hkp4py
+    :END:
+
+Though this can provide certain benefits even when working with
+ProtonMail, the scope is somewhat constrained there due to the
+limitations of the ProtonMail keyserver.
+
+For instance, searching the SKS keyserver pool for the term "gnupg"
+produces hundreds of results from any time the word appears in any
+part of a user ID.  Performing the same search on the ProtonMail
+keyserver returns zero results, even though there are at least two
+test accounts which include it as part of the username.
+
+The cause of this discrepancy is the deliberate configuration of that
+server by ProtonMail to require an exact match of the full email
+address of the ProtonMail user whose key is being requested.
+Presumably this is intended to reduce breaches of privacy of their
+users as an email address must already be known before a key for that
+address can be obtained.
+
+
+**** Import from ProtonMail via HKP for Python Example no. 1
+     :PROPERTIES:
+     :CUSTOM_ID: import-hkp4py-pm1
+     :END:
 
-Here is a variation on the first example above which checks the
-constrained ProtonMail keyserver for ProtonMail public keys.
+The following script is avalable with the rest of the examples under
+the somewhat less than original name, =pmkey-import-hkp.py=.
 
 #+BEGIN_SRC python -i
 import gpg
-import requests
+import hkp4py
+import os.path
 import sys
 
 print("""
 This script searches the ProtonMail key server for the specified key and
 imports it.
+
+Usage:  pmkey-import-hkp.py [search strings]
 """)
 
 c = gpg.Context(armor=True)
-url = "https://api.protonmail.ch/pks/lookup"
+server = hkp4py.KeyServer("hkps://api.protonmail.ch")
+keyterms = []
 ksearch = []
+allkeys = []
+results = []
+paradox = []
+homeless = None
 
-if len(sys.argv) >= 2:
+if len(sys.argv) > 2:
+    keyterms = sys.argv[1:]
+elif len(sys.argv) == 2:
     keyterm = sys.argv[1]
+    keyterms.append(keyterm)
+else:
+    key_term = input("Enter the key ID, UID or search string: ")
+    keyterms = key_term.split()
+
+for keyterm in keyterms:
+    if keyterm.count("@") == 2 and keyterm.startswith("@") is True:
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+    elif keyterm.count("@") == 1 and keyterm.startswith("@") is True:
+        ksearch.append("{0}@protonmail.com".format(keyterm[1:]))
+        ksearch.append("{0}@protonmail.ch".format(keyterm[1:]))
+        ksearch.append("{0}@pm.me".format(keyterm[1:]))
+    elif keyterm.count("@") == 0:
+        ksearch.append("{0}@protonmail.com".format(keyterm))
+        ksearch.append("{0}@protonmail.ch".format(keyterm))
+        ksearch.append("{0}@pm.me".format(keyterm))
+    elif keyterm.count("@") == 2 and keyterm.startswith("@") is False:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    elif keyterm.count("@") > 2:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    else:
+        ksearch.append(keyterm)
+
+for k in ksearch:
+    print("Checking for key for: {0}".format(k))
+    try:
+        keys = server.search(k)
+        if isinstance(keys, list) is True:
+            for key in keys:
+                allkeys.append(key)
+                try:
+                    import_result = c.key_import(key.key_blob)
+                except Exception as e:
+                    import_result = c.key_import(key.key)
+        else:
+            paradox.append(keys)
+            import_result = None
+    except Exception as e:
+        import_result = None
+    results.append(import_result)
+
+for result in results:
+    if result is not None and hasattr(result, "considered") is False:
+        print("{0} for {1}".format(result.decode(), k))
+    elif result is not None and hasattr(result, "considered") is True:
+        num_keys = len(result.imports)
+        new_revs = result.new_revocations
+        new_sigs = result.new_signatures
+        new_subs = result.new_sub_keys
+        new_uids = result.new_user_ids
+        new_scrt = result.secret_imported
+        nochange = result.unchanged
+        print("""
+The total number of keys considered for import was:  {0}
+
+With UIDs wholely or partially matching the following string:
+
+        {1}
+
+   Number of keys revoked:  {2}
+ Number of new signatures:  {3}
+    Number of new subkeys:  {4}
+   Number of new user IDs:  {5}
+Number of new secret keys:  {6}
+ Number of unchanged keys:  {7}
+
+The key IDs for all considered keys were:
+""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt,
+           nochange))
+        for i in range(num_keys):
+            print(result.imports[i].fpr)
+        print("")
+    elif result is None:
+        pass
+#+END_SRC
+
+
+**** Import from ProtonMail via HKP for Python Example no. 2
+     :PROPERTIES:
+     :CUSTOM_ID: import-hkp4py-pm2
+     :END:
+
+Like its counterpart above, this script can also be found with the
+rest of the examples, by the name pmkey-import-hkp-alt.py.
+
+With this script a modicum of effort has been made to treat anything
+passed as a =homedir= which either does not exist or which is not a
+directory, as also being a pssible user ID to check for.  It's not
+guaranteed to pick up on all such cases, but it should cover most of
+them.
+
+#+BEGIN_SRC python -i
+import gpg
+import hkp4py
+import os.path
+import sys
+
+print("""
+This script searches the ProtonMail key server for the specified key and
+imports it.  Optionally enables specifying a different GnuPG home directory.
+
+Usage:  pmkey-import-hkp.py [homedir] [search string]
+   or:  pmkey-import-hkp.py [search string]
+""")
+
+c = gpg.Context(armor=True)
+server = hkp4py.KeyServer("hkps://api.protonmail.ch")
+keyterms = []
+ksearch = []
+allkeys = []
+results = []
+paradox = []
+homeless = None
+
+if len(sys.argv) > 3:
+    homedir = sys.argv[1]
+    keyterms = sys.argv[2:]
+elif len(sys.argv) == 3:
+    homedir = sys.argv[1]
+    keyterm = sys.argv[2]
+    keyterms.append(keyterm)
+elif len(sys.argv) == 2:
+    homedir = ""
+    keyterm = sys.argv[1]
+    keyterms.append(keyterm)
 else:
     keyterm = input("Enter the key ID, UID or search string: ")
+    homedir = input("Enter the GPG configuration directory path (optional): ")
+    keyterms.append(keyterm)
 
-if keyterm.count("@") == 2 and keyterm.startswith("@") is True:
-    ksearch.append(keyterm[1:])
-    ksearch.append(keyterm[1:])
-    ksearch.append(keyterm[1:])
-elif keyterm.count("@") == 1 and keyterm.startswith("@") is True:
-    ksearch.append("{0}@protonmail.com".format(keyterm[1:]))
-    ksearch.append("{0}@protonmail.ch".format(keyterm[1:]))
-    ksearch.append("{0}@pm.me".format(keyterm[1:]))
-elif keyterm.count("@") == 0:
-    ksearch.append("{0}@protonmail.com".format(keyterm))
-    ksearch.append("{0}@protonmail.ch".format(keyterm))
-    ksearch.append("{0}@pm.me".format(keyterm))
-elif keyterm.count("@") == 2 and keyterm.startswith("@") is False:
-    uidlist = keyterm.split("@")
-    for uid in uidlist:
-        ksearch.append("{0}@protonmail.com".format(uid))
-        ksearch.append("{0}@protonmail.ch".format(uid))
-        ksearch.append("{0}@pm.me".format(uid))
-elif keyterm.count("@") > 2:
-    uidlist = keyterm.split("@")
-    for uid in uidlist:
-        ksearch.append("{0}@protonmail.com".format(uid))
-        ksearch.append("{0}@protonmail.ch".format(uid))
-        ksearch.append("{0}@pm.me".format(uid))
+if len(homedir) == 0:
+    homedir = None
+    homeless = False
+
+if homedir is not None:
+    if homedir.startswith("~"):
+        if os.path.exists(os.path.expanduser(homedir)) is True:
+            if os.path.isdir(os.path.expanduser(homedir)) is True:
+                c.home_dir = os.path.realpath(os.path.expanduser(homedir))
+            else:
+                homeless = True
+        else:
+            homeless = True
+    elif os.path.exists(os.path.realpath(homedir)) is True:
+        if os.path.isdir(os.path.realpath(homedir)) is True:
+            c.home_dir = os.path.realpath(homedir)
+        else:
+            homeless = True
+    else:
+        homeless = True
+
+# First check to see if the homedir really is a homedir and if not, treat it as
+# a search string.
+if homeless is True:
+    keyterms.append(homedir)
+    c.home_dir = None
 else:
-    ksearch.append(keyterm)
+    pass
+
+for keyterm in keyterms:
+    if keyterm.count("@") == 2 and keyterm.startswith("@") is True:
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+    elif keyterm.count("@") == 1 and keyterm.startswith("@") is True:
+        ksearch.append("{0}@protonmail.com".format(keyterm[1:]))
+        ksearch.append("{0}@protonmail.ch".format(keyterm[1:]))
+        ksearch.append("{0}@pm.me".format(keyterm[1:]))
+    elif keyterm.count("@") == 0:
+        ksearch.append("{0}@protonmail.com".format(keyterm))
+        ksearch.append("{0}@protonmail.ch".format(keyterm))
+        ksearch.append("{0}@pm.me".format(keyterm))
+    elif keyterm.count("@") == 2 and keyterm.startswith("@") is False:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    elif keyterm.count("@") > 2:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    else:
+        ksearch.append(keyterm)
 
 for k in ksearch:
-    payload = {"op": "get", "search": k}
+    print("Checking for key for: {0}".format(k))
     try:
-        r = requests.get(url, verify=True, params=payload)
-        if r.ok is True:
-            result = c.key_import(r.content)
-        elif r.ok is False:
-            result = r.content
+        keys = server.search(k)
+        if isinstance(keys, list) is True:
+            for key in keys:
+                allkeys.append(key)
+                try:
+                    import_result = c.key_import(key.key_blob)
+                except Exception as e:
+                    import_result = c.key_import(key.key)
+        else:
+            paradox.append(keys)
+            import_result = None
     except Exception as e:
-        result = None
+        import_result = None
+    results.append(import_result)
 
+for result in results:
     if result is not None and hasattr(result, "considered") is False:
         print("{0} for {1}".format(result.decode(), k))
     elif result is not None and hasattr(result, "considered") is True:
@@ -874,21 +1207,9 @@ The key IDs for all considered keys were:
             print(result.imports[i].fpr)
         print("")
     elif result is None:
-        print(e)
+        pass
 #+END_SRC
 
-Both the above example, [[../examples/howto/pmkey-import.py][pmkey-import.py]], and a version which prompts
-for an alternative GnuPG home directory, [[../examples/howto/pmkey-import-alt.py][pmkey-import-alt.py]], are
-available with the other examples and are executable scripts.
-
-Note that while the ProtonMail servers are based on the SKS servers,
-their server is related more to their API and is not feature complete
-by comparison to the servers in the SKS pool.  One notable difference
-being that the ProtonMail server does not permit non ProtonMail users
-to update their own keys, which could be a vector for attacking
-ProtonMail users who may not receive a key's revocation if it had been
-compromised.
-
 
 ** Exporting keys
    :PROPERTIES:
diff --git a/lang/python/examples/howto/pmkey-import-hkp-alt.py b/lang/python/examples/howto/pmkey-import-hkp-alt.py
new file mode 100755 (executable)
index 0000000..61fcd8d
--- /dev/null
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, division, unicode_literals
+
+import gpg
+import hkp4py
+import os.path
+import sys
+
+del absolute_import, division, unicode_literals
+
+# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org>
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License and the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License and the GNU
+# Lesser General Public along with this program; if not, see
+# <http://www.gnu.org/licenses/>.
+
+print("""
+This script searches the ProtonMail key server for the specified key and
+imports it.  Optionally enables specifying a different GnuPG home directory.
+
+Usage:  pmkey-import-hkp.py [homedir] [search string]
+   or:  pmkey-import-hkp.py [search string]
+""")
+
+c = gpg.Context(armor=True)
+server = hkp4py.KeyServer("hkps://api.protonmail.ch")
+keyterms = []
+ksearch = []
+allkeys = []
+results = []
+paradox = []
+homeless = None
+
+if len(sys.argv) > 3:
+    homedir = sys.argv[1]
+    keyterms = sys.argv[2:]
+elif len(sys.argv) == 3:
+    homedir = sys.argv[1]
+    keyterm = sys.argv[2]
+    keyterms.append(keyterm)
+elif len(sys.argv) == 2:
+    homedir = ""
+    keyterm = sys.argv[1]
+    keyterms.append(keyterm)
+else:
+    keyterm = input("Enter the key ID, UID or search string: ")
+    homedir = input("Enter the GPG configuration directory path (optional): ")
+    keyterms.append(keyterm)
+
+if len(homedir) == 0:
+    homedir = None
+    homeless = False
+
+if homedir is not None:
+    if homedir.startswith("~"):
+        if os.path.exists(os.path.expanduser(homedir)) is True:
+            if os.path.isdir(os.path.expanduser(homedir)) is True:
+                c.home_dir = os.path.realpath(os.path.expanduser(homedir))
+            else:
+                homeless = True
+        else:
+            homeless = True
+    elif os.path.exists(os.path.realpath(homedir)) is True:
+        if os.path.isdir(os.path.realpath(homedir)) is True:
+            c.home_dir = os.path.realpath(homedir)
+        else:
+            homeless = True
+    else:
+        homeless = True
+
+# First check to see if the homedir really is a homedir and if not, treat it as
+# a search string.
+if homeless is True:
+    keyterms.append(homedir)
+    c.home_dir = None
+else:
+    pass
+
+for keyterm in keyterms:
+    if keyterm.count("@") == 2 and keyterm.startswith("@") is True:
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+    elif keyterm.count("@") == 1 and keyterm.startswith("@") is True:
+        ksearch.append("{0}@protonmail.com".format(keyterm[1:]))
+        ksearch.append("{0}@protonmail.ch".format(keyterm[1:]))
+        ksearch.append("{0}@pm.me".format(keyterm[1:]))
+    elif keyterm.count("@") == 0:
+        ksearch.append("{0}@protonmail.com".format(keyterm))
+        ksearch.append("{0}@protonmail.ch".format(keyterm))
+        ksearch.append("{0}@pm.me".format(keyterm))
+    elif keyterm.count("@") == 2 and keyterm.startswith("@") is False:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    elif keyterm.count("@") > 2:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    else:
+        ksearch.append(keyterm)
+
+for k in ksearch:
+    print("Checking for key for: {0}".format(k))
+    try:
+        keys = server.search(k)
+        if isinstance(keys, list) is True:
+            for key in keys:
+                allkeys.append(key)
+                try:
+                    import_result = c.key_import(key.key_blob)
+                except Exception as e:
+                    import_result = c.key_import(key.key)
+        else:
+            paradox.append(keys)
+            import_result = None
+    except Exception as e:
+        import_result = None
+    results.append(import_result)
+
+for result in results:
+    if result is not None and hasattr(result, "considered") is False:
+        print("{0} for {1}".format(result.decode(), k))
+    elif result is not None and hasattr(result, "considered") is True:
+        num_keys = len(result.imports)
+        new_revs = result.new_revocations
+        new_sigs = result.new_signatures
+        new_subs = result.new_sub_keys
+        new_uids = result.new_user_ids
+        new_scrt = result.secret_imported
+        nochange = result.unchanged
+        print("""
+The total number of keys considered for import was:  {0}
+
+With UIDs wholely or partially matching the following string:
+
+        {1}
+
+   Number of keys revoked:  {2}
+ Number of new signatures:  {3}
+    Number of new subkeys:  {4}
+   Number of new user IDs:  {5}
+Number of new secret keys:  {6}
+ Number of unchanged keys:  {7}
+
+The key IDs for all considered keys were:
+""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt,
+           nochange))
+        for i in range(num_keys):
+            print(result.imports[i].fpr)
+        print("")
+    elif result is None:
+        pass
diff --git a/lang/python/examples/howto/pmkey-import-hkp.py b/lang/python/examples/howto/pmkey-import-hkp.py
new file mode 100755 (executable)
index 0000000..66223a9
--- /dev/null
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, division, unicode_literals
+
+import gpg
+import hkp4py
+import os.path
+import sys
+
+del absolute_import, division, unicode_literals
+
+# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org>
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License and the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License and the GNU
+# Lesser General Public along with this program; if not, see
+# <http://www.gnu.org/licenses/>.
+
+print("""
+This script searches the ProtonMail key server for the specified key and
+imports it.
+
+Usage:  pmkey-import-hkp.py [search strings]
+""")
+
+c = gpg.Context(armor=True)
+server = hkp4py.KeyServer("hkps://api.protonmail.ch")
+keyterms = []
+ksearch = []
+allkeys = []
+results = []
+paradox = []
+homeless = None
+
+if len(sys.argv) > 2:
+    keyterms = sys.argv[1:]
+elif len(sys.argv) == 2:
+    keyterm = sys.argv[1]
+    keyterms.append(keyterm)
+else:
+    key_term = input("Enter the key ID, UID or search string: ")
+    keyterms = key_term.split()
+
+for keyterm in keyterms:
+    if keyterm.count("@") == 2 and keyterm.startswith("@") is True:
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+        ksearch.append(keyterm[1:])
+    elif keyterm.count("@") == 1 and keyterm.startswith("@") is True:
+        ksearch.append("{0}@protonmail.com".format(keyterm[1:]))
+        ksearch.append("{0}@protonmail.ch".format(keyterm[1:]))
+        ksearch.append("{0}@pm.me".format(keyterm[1:]))
+    elif keyterm.count("@") == 0:
+        ksearch.append("{0}@protonmail.com".format(keyterm))
+        ksearch.append("{0}@protonmail.ch".format(keyterm))
+        ksearch.append("{0}@pm.me".format(keyterm))
+    elif keyterm.count("@") == 2 and keyterm.startswith("@") is False:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    elif keyterm.count("@") > 2:
+        uidlist = keyterm.split("@")
+        for uid in uidlist:
+            ksearch.append("{0}@protonmail.com".format(uid))
+            ksearch.append("{0}@protonmail.ch".format(uid))
+            ksearch.append("{0}@pm.me".format(uid))
+    else:
+        ksearch.append(keyterm)
+
+for k in ksearch:
+    print("Checking for key for: {0}".format(k))
+    try:
+        keys = server.search(k)
+        if isinstance(keys, list) is True:
+            for key in keys:
+                allkeys.append(key)
+                try:
+                    import_result = c.key_import(key.key_blob)
+                except Exception as e:
+                    import_result = c.key_import(key.key)
+        else:
+            paradox.append(keys)
+            import_result = None
+    except Exception as e:
+        import_result = None
+    results.append(import_result)
+
+for result in results:
+    if result is not None and hasattr(result, "considered") is False:
+        print("{0} for {1}".format(result.decode(), k))
+    elif result is not None and hasattr(result, "considered") is True:
+        num_keys = len(result.imports)
+        new_revs = result.new_revocations
+        new_sigs = result.new_signatures
+        new_subs = result.new_sub_keys
+        new_uids = result.new_user_ids
+        new_scrt = result.secret_imported
+        nochange = result.unchanged
+        print("""
+The total number of keys considered for import was:  {0}
+
+With UIDs wholely or partially matching the following string:
+
+        {1}
+
+   Number of keys revoked:  {2}
+ Number of new signatures:  {3}
+    Number of new subkeys:  {4}
+   Number of new user IDs:  {5}
+Number of new secret keys:  {6}
+ Number of unchanged keys:  {7}
+
+The key IDs for all considered keys were:
+""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt,
+           nochange))
+        for i in range(num_keys):
+            print(result.imports[i].fpr)
+        print("")
+    elif result is None:
+        pass