diff --git a/docker/dpgpid/Dockerfile b/docker/dpgpid/Dockerfile index 52ce38a..b0b999b 100644 --- a/docker/dpgpid/Dockerfile +++ b/docker/dpgpid/Dockerfile @@ -3,8 +3,8 @@ FROM python:${PYTHON_RELEASE}-alpine as dist LABEL maintainer aynic.os ARG DOCKER_BUILD_DIR=. -ARG OPERATING_SYSTEM=Linux -ARG PROCESSOR_ARCHITECTURE=x86_64 +ARG OPERATING_SYSTEM=$(uname -s) +ARG PROCESSOR_ARCHITECTURE=$(uname -m) ARG PYTHON_RELEASE=3.10 WORKDIR /opt/dpgpid @@ -19,7 +19,7 @@ RUN apk upgrade --no-cache \ && /usr/local/bin/python${PYTHON_RELEASE} -m venv ./ \ && ./bin/pip${PYTHON_RELEASE} install -U pip wheel \ && ./bin/pip${PYTHON_RELEASE} install -r ./requirements.txt \ - && wget https://github.com/libp2p/go-libp2p-core/raw/master/crypto/pb/crypto.proto \ + && wget https://raw.githubusercontent.com/libp2p/go-libp2p/master/core/crypto/pb/crypto.proto \ && protoc --python_out=./lib/python${PYTHON_RELEASE}/site-packages/ crypto.proto \ && cp -a /usr/lib/python${PYTHON_RELEASE}/site-packages/gpg ./lib/python${PYTHON_RELEASE}/site-packages/ \ && rm -rf /root/.cache ./build ./crypto.proto \ @@ -48,7 +48,7 @@ RUN apk add --no-cache \ |tar --strip-components 1 -C /opt/shellspec -xzf - \ && ln -s /opt/shellspec/shellspec ./bin/shellspec -COPY --from=ipfs/go-ipfs:v0.13.0 /usr/local/bin/ipfs ./bin/ +COPY --from=ipfs/kubo:v0.14.0 /usr/local/bin/ipfs ./bin/ COPY README.md ./ COPY COPYING ./ COPY Makefile ./ @@ -66,27 +66,38 @@ FROM dist as master ARG UID ARG USER ENV UID=${UID:-999} -ENV GID=${UID} ENV USER=dpgpid -# If we provide a specific UID -RUN let $UID >/dev/null 2>&1 \ -# Remove user with $UID if it is not our $USER - && if [ "$(getent passwd $UID |awk 'BEGIN {FS=":"} {print $1}')" != "$USER" ]; then \ - sed -i '/^'$(getent passwd $UID |awk 'BEGIN {FS=":"} {print $1}')':x:'$UID':/d' /etc/passwd; \ - sed -i '/^'$(getent group $GID |awk 'BEGIN {FS=":"} {print $1}')':x:'$GID':/d' /etc/group; \ - fi \ -# Force $UID if our $USER already exists - && sed -i 's/^'$USER':x:[0-9]\+:[0-9]\+:/'$USER':x:'$UID':'$GID':/' /etc/passwd \ - && sed -i 's/^'$USER':x:[0-9]\+:/'$USER':x:'$GID':/' /etc/group \ -# Create $USER if it does not exist - && if [ "$(getent passwd $UID)" = "" ]; then \ - echo "$USER:x:$UID:$GID::/home/$USER:/bin/false" >> /etc/passwd; \ - echo "$USER:!:$(($(date +%s) / 60 / 60 / 24)):0:99999:7:::" >> /etc/shadow; \ - echo "$USER:x:$GID:" >> /etc/group; \ - fi \ - && mkdir -p /home/$USER \ - && chown $UID:$GID /home/$USER \ - || true +# If we provide a numeric UID +RUN if [ "${UID}" -eq "${UID}" ] 2>/dev/null; then \ + # Force $UID of $USER if it exists + if [ "$(awk -F: '$1 == "'"${USER}"'" {print $3}' /etc/passwd)" != "${UID}" ]; then \ + sed -i 's/^\('"${USER}"':x\):[0-9]\+:/\1:'"${UID}"':/' /etc/passwd; \ + fi; \ + # Create $USER if $UID does not exist + if [ "$(awk -F: '$3 == "'"${UID}"'" {print $1}' /etc/passwd)" = "" ]; then \ + echo "${USER}:x:${UID}:${GID:-${UID}}::/home/${USER}:${SHELL:-/bin/sh}" >> /etc/passwd; \ + echo "${USER}:\!:$(($(date +%s) / 60 / 60 / 24)):0:99999:7:::" >> /etc/shadow; \ + mkdir -p /home/"${USER}"; \ + fi; \ + chown "${UID}" $(awk -F: '$1 == "'"${USER}"'" {print $(NF-1)}' /etc/passwd); \ +fi + +# If we provide a numeric GID +RUN if [ "${GID}" -eq "${GID}" ] 2>/dev/null; then \ + # Force $GID of $GROUP if it already exists + if [ "$(awk -F: '$1 == "'"${GROUP}"'" {print $3}' /etc/group)" != "${GID}" ]; then \ + sed -i 's/^\('"${GROUP}"':x\):[0-9]\+:/\1:'"${GID}"':/' /etc/group; \ + fi; \ + # Create $GROUP if $GID does not exist + if [ "$(awk -F: '$3 == "'"${GID}"'" {print $1}' /etc/group)" = "" ]; then \ + echo "${GROUP}:x:${GID}:" >> /etc/group; \ + fi; \ + # Force $GID of $USER if it exists + if [ "$(awk -F: '$1 == "'"${USER}"'" {print $4}' /etc/passwd)" != "${GID}" ]; then \ + sed -i 's/^\('"${USER}"':x:[0-9]\+\):[0-9]\+:/\1:'"${GID}"':/' /etc/passwd; \ + fi; \ + chgrp "${GID}" $(awk -F: '$1 == "'"${USER}"'" {print $(NF-1)}' /etc/passwd); \ +fi USER $USER diff --git a/keygen b/keygen index 00b97d8..0d029e2 100755 --- a/keygen +++ b/keygen @@ -35,12 +35,14 @@ import gpg import nacl.bindings import nacl.encoding import pgpy +import pynentry import logging as log import os import re import struct import sys import time +import warnings __version__='0.0.1' @@ -109,7 +111,6 @@ class keygen: def _check_args(self, args): log.debug("def keygen._check_args(self, args)") log.debug("self.username=%s" % self.username) - log.debug("self.password=%s" % self.password) if self.input is None: if self.password is None: if self.username is None or args.gpg is False: @@ -212,27 +213,73 @@ sec: {self.base58_secret_key} def ed25519_from_gpg(self): log.debug("def keygen.ed25519_from_gpg(self)") self.gpg_pubkeys = list(self.gpg.keylist(pattern=self.username, secret=False)) - self.gpg_seckeys = list(self.gpg.keylist(pattern=self.username, secret=True)) log.debug("self.gpg_pubkeys=%s" % self.gpg_pubkeys) + self.gpg_seckeys = list(self.gpg.keylist(pattern=self.username, secret=True)) log.debug("self.gpg_seckeys=%s" % self.gpg_seckeys) - self.gpg_seckey = self.gpg_seckeys[0] + if not self.gpg_seckeys: + log.error(f"""Unable to find any key matching username "{self.username}".""") + 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("self.gpg_seckey.__repr__=%s" % self.gpg_seckey.__repr__) log.debug("self.gpg_seckey.fpr=%s" % self.gpg_seckey.fpr) - log.debug("self.gpg_seckey.key=%s" % self.gpg_seckey.__repr__) self.armored_pgp_public_key = self.gpg.key_export(self.gpg_seckey.fpr) - self.armored_pgp_secret_key = self.gpg.key_export_secret(self.gpg_seckey.fpr) log.debug("self.armored_pgp_public_key=%s" % self.armored_pgp_public_key) + if self.password: + self.gpg.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) + self.armored_pgp_secret_key = self.gpg.key_export_secret(self.gpg_seckey.fpr) log.debug("self.armored_pgp_secret_key=%s" % self.armored_pgp_secret_key) - self.pgpy, _ = pgpy.PGPKey.from_blob(self.armored_pgp_secret_key) - log.debug("self.pgpy.fingerprint.keyid=%s" % self.pgpy.fingerprint.keyid) + if not self.armored_pgp_secret_key: + log.error(f"""Unable to export secret key id "{self.gpg_seckey.fpr}" of user "{self.username}". Please check your password!""") + exit(2) + with warnings.catch_warnings(): + # remove CryptographyDeprecationWarning about deprecated + # SymmetricKeyAlgorithm IDEA, CAST5 and Blowfish (PGPy v0.5.4) + warnings.simplefilter('ignore') + self.pgpy, _ = pgpy.PGPKey.from_blob(self.armored_pgp_secret_key) self.ed25519_from_pgpy() def ed25519_from_pgpy(self): log.debug("def keygen.ed25519_from_pgpy(self)") - self.pgpy_key_seed() + log.debug("self.pgpy.fingerprint.keyid=%s" % self.pgpy.fingerprint.keyid) + log.debug("self.pgpy.is_protected=%s" % self.pgpy.is_protected) + if self.pgpy.is_protected: + if not self.password: + with pynentry.PynEntry() as p: + p.description = f"""The exported key id "{self.pgpy.fingerprint.keyid}" of user "{self.username}" is password protected. + Please enter the passphrase again to unlock it. + """ + p.prompt = 'Passphrase:' + try: + self.password = p.get_pin() + except pynentry.PinEntryCancelled: + log.warning('Cancelled! Goodbye.') + exit(2) + try: + with warnings.catch_warnings(): + # remove CryptographyDeprecationWarning about deprecated + # SymmetricKeyAlgorithm IDEA, CAST5 and Blowfish (PGPy v0.5.4) + warnings.simplefilter('ignore') + with self.pgpy.unlock(self.password): + assert self.pgpy.is_unlocked + log.debug("self.pgpy.is_unlocked=%s" % self.pgpy.is_unlocked) + self.pgpy_key_seed() + except Exception as e: + log.error(f"""Unable to unlock secret key id "{self.gpg_seckey.fpr}" of user "{self.username}". Please check your password!""") + exit(2) + else: + self.pgpy_key_seed() self.ed25519_public_bytes, self.ed25519_secret_bytes = nacl.bindings.crypto_sign_seed_keypair(self.pgpy_key_seed) log.debug("self.ed25519_public_bytes=%s" % self.ed25519_public_bytes) log.debug("self.ed25519_secret_bytes=%s" % self.ed25519_secret_bytes) + def gpg_passphrase_cb(self, uid_hint, passphrase_info, prev_was_bad): + log.debug("uid_hint=%s" % uid_hint) + log.debug("passphrase_info=%s" % passphrase_info) + log.debug("prev_was_bad=%s" % prev_was_bad) + return self.password + def ipfs_from_ed25519(self): log.debug("def keygen.ipfs_from_ed25519(self)") @@ -275,8 +322,6 @@ sec: {self.base58_secret_key} def pgpy_key_seed(self): log.debug("def keygen.pgpy_key_seed(self)") self.pgpy_key_type() - # todo : unlock password protected key - # assert self.pgpy.is_unlocked if self.pgpy_key_type == 'RSA': log.debug("self.pgpy._key.keymaterial.p=%s" % self.pgpy._key.keymaterial.p) log.debug("self.pgpy._key.keymaterial.q=%s" % self.pgpy._key.keymaterial.q) @@ -289,7 +334,6 @@ sec: {self.base58_secret_key} log.debug("self.pgpy_key_seed=%s" % self.pgpy_key_seed) log.debug("self.pgpy_key_value=%s" % self.pgpy_key_value) log.debug("self.pgpy_key_size=%s" % self.pgpy_key_size) - log.debug("self.pgpy._key.keymaterial.encbytes=%s" % self.pgpy._key.keymaterial.encbytes) elif self.pgpy_key_type in ('ECDSA', 'EdDSA', 'ECDH'): log.debug("self.pgpy._key.keymaterial.s=%s" % self.pgpy._key.keymaterial.s) self.pgpy_key_seed = long_to_bytes(self.pgpy._key.keymaterial.s) @@ -299,7 +343,7 @@ sec: {self.base58_secret_key} log.debug("self.pgpy_key_value=%s" % self.pgpy_key_value) log.debug("self.pgpy_key_size=%s" % self.pgpy_key_size) else: - raise NotImplementedError(f"Get seed from {self.pgpy_key_type} key is not supported") + raise NotImplementedError(f"Getting seed from {self.pgpy_key_type} key is not implemented") def pgpy_key_type(self): log.debug("def keygen.pgpy_key_type(self)") @@ -344,7 +388,9 @@ sec: {self.base58_secret_key} self._check_args(args) self._load_config() # self.gpg = gpg.Context(armor=True, offline=True, homedir=GNUPGHOME) - self.gpg = gpg.Context(armor=True, offline=True) + self.gpg = gpg.Context( armor=True, + offline=True) + self.gpg.set_passphrase_cb(self.gpg_passphrase_cb) self.ed25519(args) method = getattr(self, f'do_{self.type}', self._invalid_type) return method() diff --git a/requirements.txt b/requirements.txt index 7b22c4f..ad32c67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ cryptography duniterpy google pgpy +pynentry protobuf pynacl diff --git a/specs/keygen_spec.sh b/specs/keygen_spec.sh index ebbe50e..3a9c20c 100644 --- a/specs/keygen_spec.sh +++ b/specs/keygen_spec.sh @@ -102,35 +102,53 @@ Describe 'keygen' rm -f /tmp/keygen_test_ipfs.pem End Describe '-t pgp username password birthday:' - gpg --import --quiet specs/username.asc - gpg --import --quiet specs/username.pub - It 'creates a gpg key for user username' + gpg --batch --import --quiet specs/username.asc + gpg --batch --import --quiet specs/username_protected.asc + It 'creates a password protected gpg key for user username' Skip "You should implement it !" - When run keygen -t pgp username password + When run keygen -t pgp username password birthday The status should be success End End Describe '-g -t duniter username:' It 'prints duniter keys for gpg key matching username' - When run keygen -g -t duniter username + When run keygen -g -t duniter 079E5BF4721944FB The output should include 'pub: 2g5UL2zhkn5i7oNYDpWo3fBuWvRYVU1AbMtdVmnGzPNv' The output should include 'sec: 5WtYFfA26nTfG496gAKhkrLYUMMnwXexmE1E8Q7PvtQEyscHfirsdMzW34zDp7WEkt3exNEVwoG4ajZYrm62wpi2' The status should be success The stderr should equal "" End End + Describe '-g -t duniter username@protected password:' + It 'prints duniter keys for gpg key matching username@protected locked with password' + When run keygen -g -t duniter 6222A29CBC31A087 password + The output should include 'pub: C1cRu7yb5rZhsmRHQkeZxusAhtYYJypcnXpY3HycEKsU' + The output should include 'sec: VWaEdDroSCoagJDsBnDNUtXJtKAJYdqL6XKNiomz8DtiyF44FvpiMmhidXt2j8HhDBKPZ67xBGcZPnj4Myk6cB8' + The status should be success + The stderr should equal "" + End + End Describe '-g -t ipfs username:' It 'prints ipfs keys for gpg key matching username' - When run keygen -g -t ipfs username + When run keygen -g -t ipfs 079E5BF4721944FB The output should include 'PeerID: 12D3KooWBVSe5AaQwgMCXgsxrRG8pTGk1FUBXA5eYxFeskwAtL6r' The output should include 'PrivKEY: CAESQOHXwPgzoiDca1ZnvhU/W3zdogZXulkoErnUsqt+ut82GN5k4MIbVvz2m6Vq0ij9fQFPNUz+ZZdv2D31K6mzBQc=' The status should be success The stderr should equal "" End End + Describe '-g -t ipfs username@protected password:' + It 'prints ipfs keys for gpg key matching username@protected locked with password' + When run keygen -g -t ipfs 6222A29CBC31A087 password + The output should include 'PeerID: 12D3KooWLpybeFZJGkqCHevi3MPujhx1CDbBLfu6k8BZRH8W8GbQ' + The output should include 'PrivKEY: CAESQBiV+XnBNnryoeBs6SNj9e7Cd9Xj6INn24wyxxacylYqo5idwBHJto4Vbbp6NQzuUF+e7aCmrCf6y+BSyL42/i8=' + The status should be success + The stderr should equal "" + End + End Describe '-g -o /tmp/keygen_test_duniter.pubsec -t duniter username:' It 'writes duniter keys to file for gpg key matching username' - When run keygen -g -o /tmp/keygen_test_duniter.pubsec -t duniter username + When run keygen -g -o /tmp/keygen_test_duniter.pubsec -t duniter 079E5BF4721944FB The path '/tmp/keygen_test_duniter.pubsec' should exist The contents of file '/tmp/keygen_test_duniter.pubsec' should include 'pub: 2g5UL2zhkn5i7oNYDpWo3fBuWvRYVU1AbMtdVmnGzPNv' The contents of file '/tmp/keygen_test_duniter.pubsec' should include 'sec: 5WtYFfA26nTfG496gAKhkrLYUMMnwXexmE1E8Q7PvtQEyscHfirsdMzW34zDp7WEkt3exNEVwoG4ajZYrm62wpi2' @@ -139,9 +157,20 @@ Describe 'keygen' End rm -f /tmp/keygen_test_duniter.pubsec End + Describe '-g -o /tmp/keygen_test_duniter.pubsec -t duniter username@protected password:' + It 'writes duniter keys to file for gpg key matching username@protected locked with password' + When run keygen -g -o /tmp/keygen_test_duniter.pubsec -t duniter 6222A29CBC31A087 password + The path '/tmp/keygen_test_duniter.pubsec' should exist + The contents of file '/tmp/keygen_test_duniter.pubsec' should include 'pub: C1cRu7yb5rZhsmRHQkeZxusAhtYYJypcnXpY3HycEKsU' + The contents of file '/tmp/keygen_test_duniter.pubsec' should include 'sec: VWaEdDroSCoagJDsBnDNUtXJtKAJYdqL6XKNiomz8DtiyF44FvpiMmhidXt2j8HhDBKPZ67xBGcZPnj4Myk6cB8' + The status should be success + The stderr should equal "" + End + rm -f /tmp/keygen_test_duniter.pubsec + End Describe '-g -o /tmp/keygen_test_ipfs.pem -t ipfs username:' It 'writes ipfs keys to file for gpg key matching username' - When run keygen -g -o /tmp/keygen_test_ipfs.pem -t ipfs username + When run keygen -g -o /tmp/keygen_test_ipfs.pem -t ipfs 079E5BF4721944FB The path '/tmp/keygen_test_ipfs.pem' should exist The contents of file '/tmp/keygen_test_ipfs.pem' should include '-----BEGIN PRIVATE KEY-----' The contents of file '/tmp/keygen_test_ipfs.pem' should include 'MC4CAQAwBQYDK2VwBCIEIOHXwPgzoiDca1ZnvhU/W3zdogZXulkoErnUsqt+ut82' @@ -151,4 +180,18 @@ Describe 'keygen' End rm -f /tmp/keygen_test_ipfs.pem End + Describe '-g -o /tmp/keygen_test_ipfs.pem -t ipfs username@protected password:' + It 'writes ipfs keys to file for gpg key matching username@protected locked with password' + When run keygen -g -o /tmp/keygen_test_ipfs.pem -t ipfs 6222A29CBC31A087 password + The path '/tmp/keygen_test_ipfs.pem' should exist + The contents of file '/tmp/keygen_test_ipfs.pem' should include '-----BEGIN PRIVATE KEY-----' + The contents of file '/tmp/keygen_test_ipfs.pem' should include 'MC4CAQAwBQYDK2VwBCIEIBiV+XnBNnryoeBs6SNj9e7Cd9Xj6INn24wyxxacylYq' + The contents of file '/tmp/keygen_test_ipfs.pem' should include '-----END PRIVATE KEY-----' + The status should be success + The stderr should equal "" + End + rm -f /tmp/keygen_test_ipfs.pem + End + gpg --batch --delete-secret-and-public-key --yes 4D1CDB77E91FFCD81B10F9A7079E5BF4721944FB + gpg --batch --delete-secret-and-public-key --yes 6AF574897D4979B7956AC31B6222A29CBC31A087 End diff --git a/specs/username.pub b/specs/username.pub deleted file mode 100644 index 78a1862..0000000 --- a/specs/username.pub +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -xjMEAAAAABYJKwYBBAHaRw8BAQdAGN5k4MIbVvz2m6Vq0ij9fQFPNUz+ZZdv2D31 -K6mzBQfNCHVzZXJuYW1lwmEEExYIABMFAmKD2B0JEAeeW/RyGUT7AhsDAADy8AD/ -QvC5UvW8TVUdbCd0EQvPjlfVzRauBlxQP4oy+vjnItcA/R1RgGS0D9zAGHC6CRHt -AwxzDz3dIpKQAJxxliD8ZO0G -=51r3 ------END PGP PUBLIC KEY BLOCK----- diff --git a/specs/username_protected.asc b/specs/username_protected.asc new file mode 100644 index 0000000..8def5c1 --- /dev/null +++ b/specs/username_protected.asc @@ -0,0 +1,10 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYYEAAAAABYJKwYBBAHaRw8BAQdAo5idwBHJto4Vbbp6NQzuUF+e7aCmrCf6y+BS +yL42/i/+CQMIyF96+qn63Oj/SwKcSRdzCPa5/1+HIJs/eXZFqSADs+Qj/e/XRts2 +xpcnRErR8CYZo3OzHDtlhj5sejX1nH9mErGvA3JpjO9KT+GKCF+T4s0SdXNlcm5h +bWVAcHJvdGVjdGVkwmEEExYIABMFAgAAAAAJEGIiopy8MaCHAhsDAADFPgD9Fz0e +vsLZpGbBIOE87ITJQWqAguawC57wi1YH3t+qj98BAMyos3sZjh9csrgecDPvpsjd +SOUtnZgEvISe1r4WMKUM +=ZnDt +-----END PGP PRIVATE KEY BLOCK-----