From f1b4573294148e7eec542cc7585d3c57b7c1e27b Mon Sep 17 00:00:00 2001 From: Yann Autissier Date: Thu, 29 Sep 2022 06:21:36 +0200 Subject: [PATCH] wip: first did implementation --- dpgpid | 13 ++- dpgpid.py | 245 +++++++++++++++++++++++++++++++++++++++++++ keygen => keygen.py | 43 ++++---- requirements.txt | 1 + specs/keygen_spec.sh | 4 +- 5 files changed, 280 insertions(+), 26 deletions(-) create mode 100755 dpgpid.py rename keygen => keygen.py (96%) diff --git a/dpgpid b/dpgpid index 4350b2d..a179846 100755 --- a/dpgpid +++ b/dpgpid @@ -20,7 +20,7 @@ set -eu DATE=$(date -u +%FT%TZ%Z) -USAGE='[--help] [--version] show' +USAGE='[--help] [--version] list' VERSION='0.0.1' dpgpid() { @@ -39,8 +39,8 @@ dpgpid() { -*) usage ;; - show) - show + list) + list ;; *) break @@ -54,8 +54,11 @@ help() { usage } -show() { - gpg --list-keys --with-colons |awk -F: '$1 == "pub" {print "did:pgp:"$5}' +list() { + gpg --list-secret-keys --with-colons |awk -F: '$1 == "sec" {print $5}' |while read key; do + id=$(keygen -t ipfs -g ${key} 2>/dev/null) && printf "did:ipid:${id}\n" || + echo "ERROR: Unable to extract id from key ${key}" + done } usage() { diff --git a/dpgpid.py b/dpgpid.py new file mode 100755 index 0000000..87e2c95 --- /dev/null +++ b/dpgpid.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# link: https://git.p2p.legal/aya/dpgpid/ +# desc: generate did:ipid keys from gpg + +# Copyleft 2022 Yann Autissier + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import argparse +import configparser +import gpg +import ipfshttpclient +import json +import keygen +import logging as log +from SecureBytes import clearmem +import os +import re +import struct +import sys +import time +import warnings + +__version__='0.0.1' + +class dpgpid: + def __init__(self): + self.parser = argparse.ArgumentParser(description=""" + Generate did:ipid keys from gpg. + It converts a gpg key to a Decentralized IDentifier.""") + self.parser.add_argument( + "-d", + "--debug", + action="store_true", + help="show debug informations", + ) + self.parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="show only errors", + ) + self.parser.add_argument( + "-t", + "--type", + choices=['list','show','publish','base64','duniter','ipfs','jwk'], + default="list", + dest="type", + help="output text format, default: base58", + ) + self.parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="show more informations", + ) + self.parser.add_argument( + "--version", + action="store_true", + help="show version and exit", + ) + self.parser.add_argument( + 'username', + nargs="?", + ) + self.parser.add_argument( + 'password', + nargs="?", + ) + + def _check_args(self, args): + log.debug("dpgpid._check_args(%s)" % args) + if False: + self.parser.error('dpgpid requires an input file or a username') + + def _cleanup(self): + log.debug("dpgpid._cleanup()") + self.ipfs.close() + + def _invalid_type(self): + log.debug("dpgpid._invalid_type()") + self.parser.error(f"type {self.type} is not valid.") + + def _load_config(self): + log.debug("dpgpid._load_config()") + self.config = configparser.RawConfigParser() + config_dir = os.path.join(os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), 'dpgpid') + log.debug("config_dir=%s" % config_dir) + self.config.read( [config_dir + '/dpgpid.conf'] ) + + def _run(self, argv): + args = self.parser.parse_args(argv) + vars(self).update(vars(args)) + + # display version + if args.version: + version() + sys.exit() + + # define log format + log_format='%(asctime)s %(levelname)s: %(message)s' + log_datefmt='%Y/%m/%d %H:%M:%S' + if args.debug: + log_level='DEBUG' + elif args.quiet: + log_level='ERROR' + elif args.verbose: + log_level='INFO' + else: + log_level='WARNING' + log.basicConfig(format=log_format, datefmt=log_datefmt, level=log_level) + + self._check_args(args) + self._load_config() + self.gpg = gpg.Context(armor=True, offline=True) + self.ipfs = ipfshttpclient.connect('/ip4/172.18.0.2/tcp/5001/',session=True) + method = getattr(self, f'do_{self.type}', self._invalid_type) + return method() + + def did_from_key(self): + log.debug("dpgpid.did_from_key()") + self.did = json.dumps({ +"@context": "/ipfs/QmfS56jDfrXNaS6Xcsp3RJiXd2wyY7smeEAwyTAnL1RhEG", +"id": "did:ipid:" + self.key_id, +"created": self.key_created_at, +"publicKey": [{ + "id": "did:ipid:" + self.key_id, + "type": "GpgVerificationKey2020", + "expires": self.key_expires_at, + "publicKeyGpg": self.key_public_key, +}, { + "id": "did:ipid:" + self.key_id, + "type": "JsonWebKey2020", + "expires": self.key_expires_at, + "publicKeyJwk": self.key_public_jwk, +}], +}) + if self.key_updated_at: + self.did.update({"updated": self.key_updated_at,}) + log.debug("dpgpid.did=%s" % self.did) + + def do_list(self): + log.debug("dpgpid.do_list()") + gpgkeys = list(self.gpg.keylist(pattern=self.username, secret=True)) + for gpgkey in gpgkeys: + self.key_from_gpg(gpgkey.fpr, self.password) + print("did:ipid:%s" % self.key_id) + self._cleanup() + + def do_publish(self): + log.debug("dpgpid.do_publish()") + gpgkeys = list(self.gpg.keylist(pattern=self.username, secret=True)) + if not gpgkeys: + log.warning(f"""Unable to find any key matching "{self.username}".""") + exit(1) + else: + gpgkey = gpgkeys[0] + log.info(f"""Found key id "{gpgkey.fpr}" matching "{self.username}".""") + self.key_from_gpg(gpgkey.fpr, self.password) + self.did_from_key() + self.did_cid = self.ipfs.add_json(self.did) + log.debug('dpgpid.did_cid=%s' % self.did_cid) + self.did_ipns = self.ipfs.name.publish(ipfs_path='/ipfs/' + self.did_cid, key=self.key_id)['Name'] + log.debug('dpgpid.did_ipns=%s' % self.did_ipns) + self._cleanup() + + def do_show(self): + log.debug("dpgpid.do_show()") + gpgkeys = list(self.gpg.keylist(pattern=self.username, secret=True)) + if not gpgkeys: + log.warning(f"""Unable to find any key matching "{self.username}".""") + exit(1) + else: + gpgkey = gpgkeys[0] + log.info(f"""Found key id "{gpgkey.fpr}" matching "{self.username}".""") + self.key_from_gpg(gpgkey.fpr, self.password) + self.did_from_key() + print(self.did) + self._cleanup() + + def key_from_gpg(self, username, password): + log.debug("dpgpid.key_from_gpg(%s, %s)" % (username, password)) + try: + key = keygen.keygen() + key.gpg = gpg.Context(armor=True, offline=True) + key.gpg.set_passphrase_cb(key.gpg_passphrase_cb) + key.username = username + key.password = password + key.ed25519_from_gpg() + key.pem_pkcs8_from_ed25519() + key.protobuf_from_ed25519() + key.b58mh_from_protobuf() + key.jwk_from_ed25519() + self.key_created_at = str(key.pgpy.created) + self.key_expires_at = str(key.pgpy.expires_at) + self.key_fingerprint = key.gpg_secret_key.fpr + self.key_id = key.ed25519_public_b58mh + self.key_is_expired = key.gpg_secret_key.expired + self.key_is_revoked = key.gpg_secret_key.revoked + self.key_public_key = str(key.pgpy.pubkey) + self.key_public_jwk = key.ed25519_public_jwk + self.key_secret_jwk = key.ed25519_secret_jwk + self.key_secret_pem = key.ed25519_secret_pem_pkcs8 + self.key_signers = key.pgpy.signers + self.key_uids = key.gpg_secret_key.uids + self.key_updated_at = key.gpg_secret_key.last_update + log.debug("dpgpid.key.created_at=%s" % self.key_created_at) + log.debug("dpgpid.key.expires_at=%s" % self.key_expires_at) + log.debug("dpgpid.key.fingerprint=%s" % self.key_fingerprint) + log.debug("dpgpid.key.id=%s" % self.key_id) + log.debug("dpgpid.key.is_expired=%s" % self.key_is_expired) + log.debug("dpgpid.key.is_revoked=%s" % self.key_is_revoked) + log.debug("dpgpid.key.public_key=%s" % self.key_public_key) + log.debug("dpgpid.key.public_jwk=%s" % self.key_public_jwk) + log.debug("dpgpid.key.secret_jwk=%s" % self.key_secret_jwk) + log.debug("dpgpid.key.signers=%s" % self.key_signers) + log.debug("dpgpid.key.uids=%s" % self.key_uids) + log.debug("dpgpid.key.updated_at=%s" % self.key_updated_at) + except Exception as e: + log.error(f'Unable to get key from gpg: {e}') + exit(2) + +def main(argv=None): + if argv is None: + argv = sys.argv[1:] + + cli = dpgpid() + return cli._run(argv) + +def version(version=__version__): + print("%s v%s" % (sys.argv[0],version)) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/keygen b/keygen.py similarity index 96% rename from keygen rename to keygen.py index d33e573..a7cb1b8 100755 --- a/keygen +++ b/keygen.py @@ -182,6 +182,9 @@ class keygen: clearmem(self.ipfs_privkey) log.debug("cleared: keygen.ipfs_privkey") if hasattr(self, 'jwk'): + if hasattr(self, 'ed25519_secret_jwk') and self.ed25519_secret_jwk: + clearmem(self.ed25519_secret_jwk) + log.debug("cleared: keygen.ed25519_secret_jwk") if hasattr(self.jwk, 'd') and self.jwk.d: clearmem(self.jwk.d) log.debug("cleared: keygen.jwk.d") @@ -330,7 +333,7 @@ class keygen: self._load_config() self.gpg = gpg.Context(armor=True, offline=True) self.gpg.set_passphrase_cb(self.gpg_passphrase_cb) - self.ed25519(args) + self.ed25519_from(args) method = getattr(self, f'do_{self.type}', self._invalid_type) return method() @@ -421,7 +424,7 @@ class keygen: def do_jwk(self): log.debug("keygen.do_jwk()") self.jwk_from_ed25519() - self._output(self.jwk.export_public(), self.jwk.export_private(), 'pub: ', 'sec: ') + self._output(self.ed25519_public_jwk, self.ed25519_secret_jwk, 'pub: ', 'sec: ') def duniterpy_from_credentials(self): log.debug("keygen.duniterpy_from_credentials()") @@ -591,8 +594,8 @@ class keygen: exit(2) log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed) - def ed25519(self, args): - log.debug("keygen.ed25519(%s)" % args) + def ed25519_from(self, args): + log.debug("keygen.ed25519_from(%s)" % args) if args.gpg: self.ed25519_from_gpg() else: @@ -742,6 +745,8 @@ class keygen: log.debug("keygen.jwk_from_ed25519()") try: self.jwk = jwk.JWK.from_pyca(self.ed25519) + self.ed25519_public_jwk = self.jwk.export_public() + self.ed25519_secret_jwk = self.jwk.export_private() except Exception as e: log.error(f'Unable to get jwk from ed25519: {e}') self._cleanup() @@ -769,28 +774,28 @@ class keygen: def pgpy_from_gpg(self): log.debug("keygen.pgpy_from_gpg()") try: - self.gpg_seckeys = list(self.gpg.keylist(pattern=self.username, secret=True)) - log.debug("keygen.gpg_seckeys=%s" % self.gpg_seckeys) - if not self.gpg_seckeys: - log.warning(f"""Unable to find any key matching username "{self.username}".""") + self.gpg_secret_keys = list(self.gpg.keylist(pattern=self.username, secret=True)) + log.debug("keygen.gpg_secret_keys=%s" % self.gpg_secret_keys) + if not self.gpg_secret_keys: + log.warning(f"""Unable to find any key matching "{self.username}".""") self._cleanup() exit(1) else: - self.gpg_seckey = self.gpg_seckeys[0] - log.info(f"""Found key id "{self.gpg_seckey.fpr}" matching username "{self.username}".""") - log.debug("keygen.gpg_seckey.expired=%s" % self.gpg_seckey.expired) - log.debug("keygen.gpg_seckey.fpr=%s" % self.gpg_seckey.fpr) - log.debug("keygen.gpg_seckey.revoked=%s" % self.gpg_seckey.revoked) - log.debug("keygen.gpg_seckey.uids=%s" % self.gpg_seckey.uids) - log.debug("keygen.gpg_seckey.owner_trust=%s" % self.gpg_seckey.owner_trust) - log.debug("keygen.gpg_seckey.last_update=%s" % self.gpg_seckey.last_update) + self.gpg_secret_key = self.gpg_secret_keys[0] + log.info(f"""Found key id "{self.gpg_secret_key.fpr}" matching "{self.username}".""") + log.debug("keygen.gpg_secret_key.expired=%s" % self.gpg_secret_key.expired) + log.debug("keygen.gpg_secret_key.fpr=%s" % self.gpg_secret_key.fpr) + log.debug("keygen.gpg_secret_key.revoked=%s" % self.gpg_secret_key.revoked) + log.debug("keygen.gpg_secret_key.uids=%s" % self.gpg_secret_key.uids) + log.debug("keygen.gpg_secret_key.owner_trust=%s" % self.gpg_secret_key.owner_trust) + log.debug("keygen.gpg_secret_key.last_update=%s" % self.gpg_secret_key.last_update) if self.password: self.gpg.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) - self.pgp_public_armor = self.gpg.key_export(self.gpg_seckey.fpr) - self.pgp_secret_armor = self.gpg.key_export_secret(self.gpg_seckey.fpr) + self.pgp_public_armor = self.gpg.key_export(self.gpg_secret_key.fpr) + self.pgp_secret_armor = self.gpg.key_export_secret(self.gpg_secret_key.fpr) log.debug("keygen.pgp_secret_armor=%s" % self.pgp_secret_armor) if not self.pgp_secret_armor: - log.error(f"""Unable to export gpg secret key id "{self.gpg_seckey.fpr}" of user "{self.username}". Please check your password!""") + log.error(f"""Unable to export gpg secret key id "{self.gpg_secret_key.fpr}" of user "{self.username}". Please check your password!""") self._cleanup() exit(2) with warnings.catch_warnings(): diff --git a/requirements.txt b/requirements.txt index ca8ca03..2e4230d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ base58==2.1.1 cryptography==3.4.8 duniterpy==1.1.0 +ipfshttpclient==0.7.0 jwcrypto==1.4.2 pgpy==0.5.4 pynentry==0.1.6 diff --git a/specs/keygen_spec.sh b/specs/keygen_spec.sh index 6372d82..d21c0f3 100644 --- a/specs/keygen_spec.sh +++ b/specs/keygen_spec.sh @@ -18,8 +18,8 @@ gpg() { } keygen() { - if [ -x ./keygen ]; then - GNUPGHOME="${SHELLSPEC_TMPBASE}" ./keygen "$@" + if [ -x ./keygen.py ]; then + GNUPGHOME="${SHELLSPEC_TMPBASE}" ./keygen.py "$@" elif [ -x ./bin/keygen ]; then GNUPGHOME="${SHELLSPEC_TMPBASE}" ./bin/keygen "$@" else