From 73e32a285cb9e14f930d231cfcd591158f483512 Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Wed, 12 Jun 2024 12:53:58 +0300 Subject: [PATCH 1/8] Update workflows with new and without EOLed Centos/Fedora versions. --- .github/workflows/centos-and-fedora.yml | 79 ++++++++++--------------- .github/workflows/time-machine.yml | 8 +-- 2 files changed, 36 insertions(+), 51 deletions(-) diff --git a/.github/workflows/centos-and-fedora.yml b/.github/workflows/centos-and-fedora.yml index 6b7aeb2a81..cdc7248e28 100644 --- a/.github/workflows/centos-and-fedora.yml +++ b/.github/workflows/centos-and-fedora.yml @@ -54,28 +54,23 @@ jobs: - { CC: clang, CXX: clang++, BUILD_MODE: sanitize, SHARED_LIBS: on } # All cotainers have gpg stable and lts installed -# centos-8-amd64 has botan 2.18.2 installed -# fedora-35-amd64 has botan 3.1.1 installed +# centos-9-amd64 has botan 2.19.3 installed +# fedora-39-amd64 has botan 2.19.4 installed # Any other version has to be built explicitly ! # Pls refer to https://github.com/rnpgp/rnp-ci-containers#readme for more image details image: - { name: 'CentOS 7', container: 'centos-7-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'stable' } - - { name: 'CentOS 8', container: 'centos-8-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'system' } - - { name: 'CentOS 8', container: 'centos-8-amd64', backend: 'Botan', botan_ver: '2.18.2', sm2: On, gpg_ver: 'lts' } - - { name: 'CentOS 8', container: 'centos-8-amd64', backend: 'Botan', botan_ver: '2.18.2', sm2: Off, gpg_ver: 'stable' } - - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'stable' } - - { name: 'Fedora 35', container: 'fedora-35-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'system' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'system' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan', botan_ver: '3.1.1', gpg_ver: 'system' } -# Tests against gpg head fails -# - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'head' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan', botan_ver: 'head', gpg_ver: 'system' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan', botan_ver: '3.3.0', pqc: On, gpg_ver: 'system' } - - { name: 'CentOS 8', container: 'centos-8-amd64', backend: 'OpenSSL', gpg_ver: 'lts' } - - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'OpenSSL', idea: On, gpg_ver: 'stable' } + - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'system' } + - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'Botan', botan_ver: 'system', sm2: Off, gpg_ver: 'lts' } + - { name: 'Fedora 39', container: 'fedora-39-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'system' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan', botan_ver: 'system', gpg_ver: 'system' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan', botan_ver: '3.1.1', gpg_ver: 'system' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan', botan_ver: 'head', gpg_ver: 'system' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan', botan_ver: '3.3.0', pqc: On, gpg_ver: 'system' } + - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'OpenSSL', gpg_ver: 'lts' } - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'OpenSSL', idea: Off,gpg_ver: 'stable' } - - { name: 'Fedora 35', container: 'fedora-35-amd64', backend: 'OpenSSL', gpg_ver: 'system' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'OpenSSL', gpg_ver: 'system' } + - { name: 'Fedora 39', container: 'fedora-39-amd64', backend: 'OpenSSL', gpg_ver: 'system' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'OpenSSL', gpg_ver: 'system' } # There is some ABI incompatibility between llvm-7, bitan shared library from ribose repo and sanitizer # So we are enforving static lib for sanitizers on CentOS 7 @@ -86,32 +81,23 @@ jobs: - image: { name: 'CentOS 7', container: 'centos-7-amd64', gpg_ver: stable, backend: Botan, botan_ver: 'system' } env: { CC: clang, CXX: clang++, BUILD_MODE: sanitize, SHARED_LIBS: off } # Coverage report for Botan 2.x backend - - image: { name: 'CentOS 8', container: 'centos-8-amd64', gpg_ver: stable, backend: Botan, botan_ver: '2.18.2' } + - image: { name: 'CentOS 9 Coverage', container: 'centos-9-amd64', gpg_ver: stable, backend: Botan, botan_ver: 'system' } env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } # Coverage report for Botan 3.x backend - - image: { name: 'Fedora 36', container: 'fedora-36-amd64', gpg_ver: stable, backend: Botan, botan_ver: '3.1.1' } - env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } - # Coverage report for OpenSSL 1.1.1 backend - - image: { name: 'CentOS 8', container: 'centos-8-amd64', gpg_ver: stable, backend: OpenSSL } + - image: { name: 'Fedora 40 Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: Botan, botan_ver: '3.3.0' } env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } # Coverage report for OpenSSL 3.0 backend - - image: { name: 'Fedora 36', container: 'fedora-36-amd64', gpg_ver: stable, backend: OpenSSL } + - image: { name: 'Fedora 40 Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: OpenSSL } env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } # Coverage report for OpenSSL 3.0 backend with disabled algos - - image: { name: 'Fedora 36', container: 'fedora-36-amd64', gpg_ver: stable, backend: OpenSSL, idea: Off, sm2: Off, two: Off, blow: Off, rmd: Off, bp: Off } + - image: { name: 'Fedora 40 Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: OpenSSL, idea: Off, sm2: Off, two: Off, blow: Off, rmd: Off, bp: Off } env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } # Coverage report for Botan backend with disabled algos - - image: { name: 'Fedora 36', container: 'fedora-36-amd64', gpg_ver: stable, backend: Botan, idea: Off, sm2: Off, two: Off, blow: Off, rmd: Off, bp: Off } + - image: { name: 'Fedora 40 Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: Botan, idea: Off, sm2: Off, two: Off, blow: Off, rmd: Off, bp: Off } env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } - # Coverage report for PQC - - image: { name: 'Fedora 36', container: 'fedora-36-amd64', gpg_ver: stable, backend: Botan, botan_ver: '3.3.0', pqc: On } - env: { CC: clang, CXX: clang++, BUILD_MODE: sanitize, SHARED_LIBS: off } - # Fedora 38 - - image: { name: 'Fedora 38', container: 'fedora-38-amd64', gpg_ver: system, backend: Botan, botan_ver: 'system' } - env: { CC: gcc, CXX: g++, BUILD_MODE: normal, SHARED_LIBS: on } - # Fedora 39 - - image: { name: 'Fedora 38', container: 'fedora-39-amd64', gpg_ver: system, backend: Botan, botan_ver: 'system' } - env: { CC: gcc, CXX: g++, BUILD_MODE: normal, SHARED_LIBS: on } + # Coverage report for PQC - not running yet due to very low coverage + #- image: { name: 'Fedora 40 PQC Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: Botan, botan_ver: '3.3.0', pqc: On } + # env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: off } container: ghcr.io/rnpgp/ci-rnp-${{ matrix.image.container }} @@ -242,10 +228,9 @@ jobs: matrix: image: - { name: 'CentOS 7', container: 'centos-7-amd64' } - - { name: 'CentOS 8', container: 'centos-8-amd64' } - { name: 'CentOS 9', container: 'centos-9-amd64' } - - { name: 'Fedora 35', container: 'fedora-35-amd64' } - - { name: 'Fedora 36', container: 'fedora-36-amd64' } + - { name: 'Fedora 39', container: 'fedora-39-amd64' } + - { name: 'Fedora 40', container: 'fedora-40-amd64' } name: Package ${{ matrix.image.name }} SRPM @@ -313,10 +298,9 @@ jobs: matrix: image: - { name: 'CentOS 7', container: 'centos-7-amd64' } - - { name: 'CentOS 8', container: 'centos-8-amd64' } - { name: 'CentOS 9', container: 'centos-9-amd64' } - - { name: 'Fedora 35', container: 'fedora-35-amd64' } - - { name: 'Fedora 36', container: 'fedora-36-amd64' } + - { name: 'Fedora 39', container: 'fedora-39-amd64' } + - { name: 'Fedora 40', container: 'fedora-40-amd64' } name: Package ${{ matrix.image.name }} RPM steps: @@ -381,23 +365,24 @@ jobs: matrix: image: - { name: 'CentOS 7', container: 'centos:7' } - - { name: 'CentOS 8', container: 'tgagor/centos:stream8' } - { name: 'CentOS 9', container: 'quay.io/centos/centos:stream9' } - - { name: 'Fedora 35', container: 'fedora:35' } - - { name: 'Fedora 36', container: 'fedora:36' } + # Fedora 39 is disabled since it has cmake issue which prevents man pages to be packaged. + # Please see package step for error message. + #- { name: 'Fedora 39', container: 'fedora:39' } + - { name: 'Fedora 40', container: 'fedora:40' } name: RPM test on ${{ matrix.image.name }} steps: - name: Install prerequisites run: yum -y install sudo wget binutils -# CentOS 7/8 packages depend on botan.so.16 that gets installed from ribose repo -# Fedora 35/36 packages depend on botan.so.19 that comes Fedora package, that is available by default +# CentOS 7 packages depend on botan.so.16 that gets installed from ribose repo +# Fedora 39/40 packages depend on botan.so.19 that comes Fedora package, that is available by default # CentOS 9 depend on botan.so.19 and needs EPEL9 repo that needs to be installed # ribose repo is also a source of json-c (v12 aka json-c12) for CentOS 7 - name: Install ribose-packages - if: matrix.image.container == 'centos:7' || matrix.image.container == 'tgagor/centos:stream8' + if: matrix.image.container == 'centos:7' run: | sudo rpm --import https://github.com/riboseinc/yum/raw/master/ribose-packages-next.pub sudo wget https://github.com/riboseinc/yum/raw/master/ribose.repo -O /etc/yum.repos.d/ribose.repo @@ -410,7 +395,7 @@ jobs: sudo dnf -y install epel-release - name: Install xargs - if: matrix.image.container == 'fedora:35' + if: matrix.image.container == 'fedora:39' run: sudo yum -y install findutils - name: Download rnp rpms v3 diff --git a/.github/workflows/time-machine.yml b/.github/workflows/time-machine.yml index 87d4ab3d33..7f69ce6f82 100644 --- a/.github/workflows/time-machine.yml +++ b/.github/workflows/time-machine.yml @@ -78,8 +78,8 @@ jobs: # but uses just few of tehm # Pls refer to https://github.com/rnpgp/rnp-ci-containers#readme for image details image: - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'OpenSSL' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'OpenSSL' } container: ghcr.io/rnpgp/ci-rnp-${{ matrix.image.container }} @@ -138,8 +138,8 @@ jobs: # Pls refer to https://github.com/rnpgp/rnp-ci-containers#readme for image details image: - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'Botan' } - - { name: 'Fedora 36', container: 'fedora-36-amd64', backend: 'OpenSSL' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan' } + - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'OpenSSL' } date-offset: - '+0y' From 47d1a795a377a50ef495c17f03d09067547ddece Mon Sep 17 00:00:00 2001 From: "Maxim [maxirmx] Samsonov" Date: Mon, 17 Jun 2024 10:05:39 +0300 Subject: [PATCH 2/8] Fixed MaxOS 13 python update issue --- .github/workflows/macos.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d538724771..024becf28d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -103,7 +103,11 @@ jobs: echo "CRYPTO_BACKEND=openssl" >> $GITHUB_ENV - name: Install dependencies - run: brew bundle + run: | + rm -f '/usr/local/bin/2to3' 'usr/local/bin/2to3-3.12' '/usr/local/bin/idle3' '/usr/local/bin/idle3.12' \ + '/usr/local/bin/pydoc3' '/usr/local/bin/pydoc3.12' '/usr/local/bin/python3' '/usr/local/bin/python3-config' \ + '/usr/local/bin/python3.12' '/usr/local/bin/python3.12-config' + brew bundle - name: Botan2 cache id: cache From a708cb83d05ac5888b6ad0395cbc021319d68313 Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Tue, 4 Jun 2024 14:05:36 +0300 Subject: [PATCH 3/8] Improve coding style for aead decryption. --- src/lib/crypto/symmetric.cpp | 5 ++--- src/librepgp/stream-parse.cpp | 28 +++++++++++----------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/lib/crypto/symmetric.cpp b/src/lib/crypto/symmetric.cpp index ee1531b46b..889b0f8fb9 100644 --- a/src/lib/crypto/symmetric.cpp +++ b/src/lib/crypto/symmetric.cpp @@ -279,14 +279,13 @@ pgp_cipher_aead_start(pgp_crypt_t *crypt, const uint8_t *nonce, size_t len) bool pgp_cipher_aead_update(pgp_crypt_t *crypt, uint8_t *out, const uint8_t *in, size_t len) { - size_t outwr = 0; - size_t inread = 0; - if (len % crypt->aead.granularity) { RNP_LOG("aead wrong update len"); return false; } + size_t outwr = 0; + size_t inread = 0; if (botan_cipher_update(crypt->aead.obj, 0, out, len, &outwr, in, len, &inread) != 0) { RNP_LOG("aead update failed"); return false; diff --git a/src/librepgp/stream-parse.cpp b/src/librepgp/stream-parse.cpp index 129c99514d..1f3ce9dacc 100644 --- a/src/librepgp/stream-parse.cpp +++ b/src/librepgp/stream-parse.cpp @@ -483,9 +483,6 @@ compressed_src_close(pgp_source_t *src) static bool encrypted_start_aead_chunk(pgp_source_encrypted_param_t *param, size_t idx, bool last) { - uint8_t nonce[PGP_AEAD_MAX_NONCE_LEN]; - size_t nlen; - size_t default_ad_len = param->aead_adlen; if (last) { @@ -550,7 +547,8 @@ encrypted_start_aead_chunk(pgp_source_encrypted_param_t *param, size_t idx, bool param->chunkin = 0; /* set chunk index for nonce */ - nlen = pgp_cipher_aead_nonce(param->aead_hdr.aalg, param->aead_hdr.iv, nonce, idx); + uint8_t nonce[PGP_AEAD_MAX_NONCE_LEN]; + size_t nlen = pgp_cipher_aead_nonce(param->aead_hdr.aalg, param->aead_hdr.iv, nonce, idx); /* start cipher */ return pgp_cipher_aead_start(¶m->decrypt, nonce, nlen); @@ -560,13 +558,6 @@ encrypted_start_aead_chunk(pgp_source_encrypted_param_t *param, size_t idx, bool static bool encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) { - bool lastchunk = false; - bool chunkend = false; - bool res = false; - size_t read; - size_t tagread; - size_t taglen; - param->cachepos = 0; param->cachelen = 0; @@ -575,8 +566,9 @@ encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) } /* it is always 16 for defined EAX and OCB, however this may change in future */ - taglen = pgp_cipher_aead_tag_len(param->aead_hdr.aalg); - read = sizeof(param->cache) - 2 * PGP_AEAD_MAX_TAG_LEN; + size_t taglen = pgp_cipher_aead_tag_len(param->aead_hdr.aalg); + size_t read = sizeof(param->cache) - 2 * PGP_AEAD_MAX_TAG_LEN; + bool chunkend = false; if (read >= param->chunklen - param->chunkin) { read = param->chunklen - param->chunkin; @@ -590,10 +582,12 @@ encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) } /* checking whether we have enough input for the final tags */ + size_t tagread = 0; if (!param->pkt.readsrc->peek(param->cache + read, taglen * 2, &tagread)) { return false; } + bool lastchunk = false; if (tagread < taglen * 2) { /* this would mean the end of the stream */ if ((param->chunkin == 0) && (read + tagread == taglen)) { @@ -610,6 +604,7 @@ encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) } } + bool res = false; if (!chunkend && !lastchunk) { param->chunkin += read; res = pgp_cipher_aead_update(¶m->decrypt, param->cache, param->cache, read); @@ -669,13 +664,12 @@ encrypted_src_read_aead(pgp_source_t *src, void *buf, size_t len, size_t *read) #if !defined(ENABLE_AEAD) return false; #else - pgp_source_encrypted_param_t *param = (pgp_source_encrypted_param_t *) src->param; - size_t cbytes; - size_t left = len; + auto param = (pgp_source_encrypted_param_t *) src->param; + size_t left = len; do { /* check whether we have something in the cache */ - cbytes = param->cachelen - param->cachepos; + size_t cbytes = param->cachelen - param->cachepos; if (cbytes > 0) { if (cbytes >= left) { memcpy(buf, param->cache + param->cachepos, left); From 2ee24c1e1863b33dce956d5039c42e775b052b9a Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Tue, 28 May 2024 17:27:58 +0300 Subject: [PATCH 4/8] Correctly handle the case when pgp_cipher_aead_update() may not process the whole input. --- src/lib/crypto/symmetric.cpp | 12 ++-- src/lib/crypto/symmetric.h | 4 +- src/lib/crypto/symmetric_ossl.cpp | 6 +- src/librepgp/stream-def.h | 2 +- src/librepgp/stream-parse.cpp | 102 ++++++++++++++++++------------ src/librepgp/stream-write.cpp | 13 +++- 6 files changed, 87 insertions(+), 52 deletions(-) diff --git a/src/lib/crypto/symmetric.cpp b/src/lib/crypto/symmetric.cpp index 889b0f8fb9..8f713e0b58 100644 --- a/src/lib/crypto/symmetric.cpp +++ b/src/lib/crypto/symmetric.cpp @@ -277,25 +277,27 @@ pgp_cipher_aead_start(pgp_crypt_t *crypt, const uint8_t *nonce, size_t len) } bool -pgp_cipher_aead_update(pgp_crypt_t *crypt, uint8_t *out, const uint8_t *in, size_t len) +pgp_cipher_aead_update( + pgp_crypt_t &crypt, uint8_t *out, const uint8_t *in, size_t len, size_t &read) { - if (len % crypt->aead.granularity) { + if (len % crypt.aead.granularity) { RNP_LOG("aead wrong update len"); return false; } size_t outwr = 0; size_t inread = 0; - if (botan_cipher_update(crypt->aead.obj, 0, out, len, &outwr, in, len, &inread) != 0) { + if (botan_cipher_update(crypt.aead.obj, 0, out, len, &outwr, in, len, &inread)) { RNP_LOG("aead update failed"); return false; } - if ((outwr != len) || (inread != len)) { - RNP_LOG("wrong aead usage"); + if (outwr != inread) { + RNP_LOG("wrong aead usage: %zu vs %zu, len is %zu", outwr, inread, len); return false; } + read = inread; return true; } diff --git a/src/lib/crypto/symmetric.h b/src/lib/crypto/symmetric.h index 33c605c4c7..f2ab927410 100644 --- a/src/lib/crypto/symmetric.h +++ b/src/lib/crypto/symmetric.h @@ -213,10 +213,12 @@ bool pgp_cipher_aead_start(pgp_crypt_t *crypt, const uint8_t *nonce, size_t len) * len bytes * @param in buffer with input, cannot be NULL * @param len number of bytes to process. Should be multiple of update granularity. + * @param read number of bytes read and processed, in rare cases could be less then len. * @return true on success or false otherwise. On success exactly len processed bytes will be * stored in out buffer */ -bool pgp_cipher_aead_update(pgp_crypt_t *crypt, uint8_t *out, const uint8_t *in, size_t len); +bool pgp_cipher_aead_update( + pgp_crypt_t &crypt, uint8_t *out, const uint8_t *in, size_t len, size_t &read); /** @brief Do final update on the cipher. For decryption final chunk should contain at least * authentication tag, for encryption input could be zero-size. diff --git a/src/lib/crypto/symmetric_ossl.cpp b/src/lib/crypto/symmetric_ossl.cpp index 5c19ee6a9f..e9a42aed91 100644 --- a/src/lib/crypto/symmetric_ossl.cpp +++ b/src/lib/crypto/symmetric_ossl.cpp @@ -268,17 +268,19 @@ pgp_cipher_aead_start(pgp_crypt_t *crypt, const uint8_t *nonce, size_t len) } bool -pgp_cipher_aead_update(pgp_crypt_t *crypt, uint8_t *out, const uint8_t *in, size_t len) +pgp_cipher_aead_update( + pgp_crypt_t &crypt, uint8_t *out, const uint8_t *in, size_t len, size_t &read) { if (!len) { return true; } int out_len = 0; - bool res = EVP_CipherUpdate(crypt->aead.obj, out, &out_len, in, len) == 1; + bool res = EVP_CipherUpdate(crypt.aead.obj, out, &out_len, in, len) == 1; if (!res) { RNP_LOG("Failed to update cipher: %lu", ERR_peek_last_error()); // LCOV_EXCL_LINE } assert(out_len == (int) len); + read = len; return res; } diff --git a/src/librepgp/stream-def.h b/src/librepgp/stream-def.h index 7a1108f827..583bfb4110 100644 --- a/src/librepgp/stream-def.h +++ b/src/librepgp/stream-def.h @@ -54,7 +54,7 @@ #define ST_FROM ("From") /* Preallocated cache length for AEAD encryption/decryption */ -#define PGP_AEAD_CACHE_LEN (PGP_INPUT_CACHE_SIZE + PGP_AEAD_MAX_TAG_LEN) +#define PGP_AEAD_CACHE_LEN (PGP_INPUT_CACHE_SIZE + 2 * PGP_AEAD_MAX_TAG_LEN) /* Maximum OpenPGP packet nesting level */ #define MAXIMUM_NESTING_LEVEL 32 diff --git a/src/librepgp/stream-parse.cpp b/src/librepgp/stream-parse.cpp index 1f3ce9dacc..325363d441 100644 --- a/src/librepgp/stream-parse.cpp +++ b/src/librepgp/stream-parse.cpp @@ -96,14 +96,15 @@ typedef struct pgp_source_encrypted_param_t { size_t chunklen{}; /* size of AEAD chunk in bytes */ size_t chunkin{}; /* number of bytes read from the current chunk */ size_t chunkidx{}; /* index of the current chunk */ - uint8_t cache[PGP_AEAD_CACHE_LEN]; /* read cache */ - size_t cachelen{}; /* number of bytes in the cache */ - size_t cachepos{}; /* index of first unread byte in the cache */ - pgp_aead_hdr_t aead_hdr; /* AEAD encryption parameters */ - uint8_t aead_ad[PGP_AEAD_MAX_AD_LEN]; /* additional data */ - size_t aead_adlen{}; /* length of the additional data */ - pgp_symm_alg_t salg; /* data encryption algorithm */ - pgp_parse_handler_t * handler{}; /* parsing handler with callbacks */ + size_t rawbytes{}; /* number of bytes in cache read but not decrypted */ + uint8_t cache[PGP_AEAD_CACHE_LEN]; /* read cache */ + size_t cachelen{}; /* number of bytes in the cache */ + size_t cachepos{}; /* index of first unread byte in the cache */ + pgp_aead_hdr_t aead_hdr; /* AEAD encryption parameters */ + uint8_t aead_ad[PGP_AEAD_MAX_AD_LEN]; /* additional data */ + size_t aead_adlen{}; /* length of the additional data */ + pgp_symm_alg_t salg; /* data encryption algorithm */ + pgp_parse_handler_t *handler{}; /* parsing handler with callbacks */ #ifdef ENABLE_CRYPTO_REFRESH pgp_seipdv2_hdr_t seipdv2_hdr; /* SEIPDv2 encryption parameters */ #endif @@ -558,6 +559,10 @@ encrypted_start_aead_chunk(pgp_source_encrypted_param_t *param, size_t idx, bool static bool encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) { + /* Check whether we have some bytes which were read but not decrypted */ + if (param->rawbytes) { + memcpy(param->cache, param->cache + param->cachelen, param->rawbytes); + } param->cachepos = 0; param->cachelen = 0; @@ -567,34 +572,38 @@ encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) /* it is always 16 for defined EAX and OCB, however this may change in future */ size_t taglen = pgp_cipher_aead_tag_len(param->aead_hdr.aalg); - size_t read = sizeof(param->cache) - 2 * PGP_AEAD_MAX_TAG_LEN; + size_t read = sizeof(param->cache) - 2 * PGP_AEAD_MAX_TAG_LEN - param->rawbytes; bool chunkend = false; if (read >= param->chunklen - param->chunkin) { - read = param->chunklen - param->chunkin; + /* param->rawbytes is smaller then param->chunklen - param->chunkin due to the previous + * call */ + read = param->chunklen - param->chunkin - param->rawbytes; chunkend = true; } else { - read = read - read % pgp_cipher_aead_granularity(¶m->decrypt); + read = read - (read + param->rawbytes) % pgp_cipher_aead_granularity(¶m->decrypt); } - if (!param->pkt.readsrc->read(param->cache, read, &read)) { + if (!param->pkt.readsrc->read(param->cache + param->rawbytes, read, &read)) { return false; } /* checking whether we have enough input for the final tags */ size_t tagread = 0; - if (!param->pkt.readsrc->peek(param->cache + read, taglen * 2, &tagread)) { + if (!param->pkt.readsrc->peek( + param->cache + param->rawbytes + read, taglen * 2, &tagread)) { return false; } - bool lastchunk = false; + bool lastchunk = false; + size_t avail = param->rawbytes + read + tagread; if (tagread < taglen * 2) { /* this would mean the end of the stream */ - if ((param->chunkin == 0) && (read + tagread == taglen)) { + if ((param->chunkin == 0) && (avail == taglen)) { /* we have empty chunk and final tag */ chunkend = false; lastchunk = true; - } else if (read + tagread >= 2 * taglen) { + } else if (avail >= 2 * taglen) { /* we have end of chunk and final tag */ chunkend = true; lastchunk = true; @@ -604,57 +613,69 @@ encrypted_src_read_aead_part(pgp_source_encrypted_param_t *param) } } - bool res = false; if (!chunkend && !lastchunk) { - param->chunkin += read; - res = pgp_cipher_aead_update(¶m->decrypt, param->cache, param->cache, read); + size_t used = 0; + bool res = pgp_cipher_aead_update( + param->decrypt, param->cache, param->cache, param->rawbytes + read, used); + + param->chunkin += used; if (res) { - param->cachelen = read; + param->cachelen = used; + param->rawbytes = param->rawbytes + read - used; } return res; } + /* Processing end of chunk */ if (chunkend) { if (tagread > taglen) { param->pkt.readsrc->skip(tagread - taglen); } - res = pgp_cipher_aead_finish( - ¶m->decrypt, param->cache, param->cache, read + tagread - taglen); - if (!res) { + if (!pgp_cipher_aead_finish(¶m->decrypt, + param->cache, + param->cache, + param->rawbytes + read + tagread - taglen)) { RNP_LOG("failed to finalize aead chunk"); - return res; + return false; } - param->cachelen = read + tagread - 2 * taglen; + param->cachelen = param->rawbytes + read + tagread - 2 * taglen; param->chunkin += param->cachelen; + param->rawbytes = 0; } + /* Starting a new chunk */ size_t chunkidx = param->chunkidx; if (chunkend && param->chunkin) { chunkidx++; } - if (!(res = encrypted_start_aead_chunk(param, chunkidx, lastchunk))) { + if (!encrypted_start_aead_chunk(param, chunkidx, lastchunk)) { RNP_LOG("failed to start aead chunk"); - return res; + return false; } - if (lastchunk) { - if (tagread > 0) { - param->pkt.readsrc->skip(tagread); - } + if (!lastchunk) { + return true; + } - size_t off = read + tagread - taglen; - res = pgp_cipher_aead_finish( - ¶m->decrypt, param->cache + off, param->cache + off, taglen); - if (!res) { - RNP_LOG("wrong last chunk"); - return res; - } - param->auth_validated = true; + /* Processing last chunk */ + if (tagread > 0) { + param->pkt.readsrc->skip(tagread); } - return res; + /* Probably math below could be improved. The reason of this is that for chunkend we set + * rawbytes to 0 but it still be useful. */ + size_t off = + chunkend ? param->cachelen + taglen : param->rawbytes + read + tagread - taglen; + if (!pgp_cipher_aead_finish( + ¶m->decrypt, param->cache + off, param->cache + off, taglen)) { + RNP_LOG("wrong last chunk"); + return false; + } + param->rawbytes = 0; + param->auth_validated = true; + return true; } #endif @@ -683,7 +704,6 @@ encrypted_src_read_aead(pgp_source_t *src, void *buf, size_t len, size_t *read) memcpy(buf, param->cache + param->cachepos, cbytes); buf = (uint8_t *) buf + cbytes; left -= cbytes; - param->cachepos = param->cachelen = 0; } /* read something into cache */ diff --git a/src/librepgp/stream-write.cpp b/src/librepgp/stream-write.cpp index cf6d2e4d91..9b72438473 100644 --- a/src/librepgp/stream-write.cpp +++ b/src/librepgp/stream-write.cpp @@ -467,7 +467,8 @@ encrypted_dst_write_aead(pgp_dest_t *dst, const void *buf, size_t len) } while (len > 0) { - sz = std::min(sizeof(param->cache) - PGP_AEAD_MAX_TAG_LEN - param->cachelen, len); + /* 2 tags to align to the PGP_INPUT_CACHE_SIZE size */ + sz = std::min(sizeof(param->cache) - 2 * PGP_AEAD_MAX_TAG_LEN - param->cachelen, len); sz = std::min(sz, param->chunklen - param->chunkout - param->cachelen); memcpy(param->cache + param->cachelen, buf, sz); param->cachelen += sz; @@ -481,9 +482,17 @@ encrypted_dst_write_aead(pgp_dest_t *dst, const void *buf, size_t len) } else if (param->cachelen >= gran) { /* we have part of the chunk - so need to adjust it to the granularity */ size_t gransz = param->cachelen - param->cachelen % gran; - if (!pgp_cipher_aead_update(¶m->encrypt, param->cache, param->cache, gransz)) { + size_t inread = 0; + if (!pgp_cipher_aead_update( + param->encrypt, param->cache, param->cache, gransz, inread)) { return RNP_ERROR_BAD_STATE; } + if (inread != gransz) { + /* LCOV_EXCL_START */ + RNP_LOG("Unexpected aead update: read %zu instead of %zu.", inread, gransz); + return RNP_ERROR_BAD_STATE; + /* LCOV_EXCL_END */ + } dst_write(param->pkt.writedst, param->cache, gransz); memmove(param->cache, param->cache + gransz, param->cachelen - gransz); param->cachelen -= gransz; From 98e8043db69fd1099feec90c4bb5c3c7513f1634 Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Tue, 11 Jun 2024 15:24:34 +0300 Subject: [PATCH 5/8] Add test for the issue #2245: Botan 3.5 compatibility on aead decryption. --- src/tests/cli_tests.py | 16 ++++++++++++++++ .../data/test_messages/cleartext.rnp-aead-eax | Bin 0 -> 41665 bytes .../data/test_messages/pubkey-aead-eax.gpg | Bin 0 -> 4704 bytes .../data/test_messages/seckey-aead-eax.gpg | Bin 0 -> 9878 bytes 4 files changed, 16 insertions(+) create mode 100644 src/tests/data/test_messages/cleartext.rnp-aead-eax create mode 100644 src/tests/data/test_messages/pubkey-aead-eax.gpg create mode 100644 src/tests/data/test_messages/seckey-aead-eax.gpg diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 634c88504e..c5be6d8c68 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -4654,6 +4654,22 @@ def test_encryption_no_wrap(self): self.assertEqual(file_text(dec), file_text(src)) clear_workfiles() + def test_aead_eax_botan35_decryption(self): + # Artifact which was obtained from tests used EAX + Twofish + if not RNP_AEAD_EAX or not RNP_TWOFISH: + self.skipTest('AEAD-EAX is not supported') + # See issue #2245 for the details + [dec] = reg_workfiles('cleartext', '.txt') + EAXSRC = data_path('test_messages/cleartext.rnp-aead-eax') + # Decrypt and verify AEAD-EAX encrypted message by RNP + ret, _, _ = run_proc(RNP, ['--keyfile', data_path('test_messages/seckey-aead-eax.gpg'), '--password', 'encsign1pass', '-d', EAXSRC, '--output', dec]) + self.assertEqual(ret, 0) + remove_files(dec) + # Decrypt it using the password + ret, _, _ = run_proc(RNP, ['--keyfile', data_path('test_messages/pubkey-aead-eax.gpg'), '--password', 'password1', '-d', EAXSRC, '--output', dec]) + self.assertEqual(ret, 0) + clear_workfiles() + class Compression(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/src/tests/data/test_messages/cleartext.rnp-aead-eax b/src/tests/data/test_messages/cleartext.rnp-aead-eax new file mode 100644 index 0000000000000000000000000000000000000000..c338d09136ea2ccab3e18fd8f665e496d579a531 GIT binary patch literal 41665 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 h)mQ}% literal 0 HcmV?d00001 diff --git a/src/tests/data/test_messages/pubkey-aead-eax.gpg b/src/tests/data/test_messages/pubkey-aead-eax.gpg new file mode 100644 index 0000000000000000000000000000000000000000..df5dfc470297e99830ba200aecc4c22cd33a6119 GIT binary patch literal 4704 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 literal 0 HcmV?d00001 From f7f4bbe21fcd6e9a260c5b20bc6aca01b8dbd42c Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Tue, 11 Jun 2024 16:33:27 +0300 Subject: [PATCH 6/8] Fix a bunch of SonarLint warnings. --- src/tests/cli_tests.py | 113 +++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index c5be6d8c68..ebea4a7517 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -100,6 +100,7 @@ def escape_regex(str): PUBRING_1 = 'keyrings/1/pubring.gpg' SECRING_1 = 'keyrings/1/secring.gpg' KEYRING_DIR_1 = 'keyrings/1' +KEYRING_DIR_2 = 'keyrings/2' KEYRING_DIR_3 = 'keyrings/3' SECRING_G10 = 'test_stream_key_load/g10' KEY_ALICE_PUB = 'test_key_validity/alice-pub.asc' @@ -521,7 +522,7 @@ def gpg_encrypt_file(src, dst, cipher=None, z=None, armor=False): if armor: params[2:2] = ['--armor'] if GPG_NO_OLD: params[2:2] = ['--allow-old-cipher-algos'] - ret, out, err = run_proc(GPG, params) + ret, _, err = run_proc(GPG, params) if ret != 0: raise_err('gpg encryption failed for cipher ' + cipher, err) @@ -541,7 +542,7 @@ def gpg_symencrypt_file(src, dst, cipher=None, z=None, armor=False, aead=None): params[3:3] = ['--chunk-size', str(aead[1] + 6)] params[3:3] = ['--rfc4880bis', '--force-aead'] - ret, out, err = run_proc(GPG, params) + ret, _, err = run_proc(GPG, params) if ret != 0: raise_err('gpg symmetric encryption failed for cipher ' + cipher, err) @@ -549,7 +550,7 @@ def gpg_symencrypt_file(src, dst, cipher=None, z=None, armor=False, aead=None): def gpg_decrypt_file(src, dst, keypass): src = path_for_gpg(src) dst = path_for_gpg(dst) - ret, out, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, GPG_LOOPBACK, '--batch', + ret, _, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, GPG_LOOPBACK, '--batch', '--yes', '--passphrase', keypass, '--trust-model', 'always', '-o', dst, '-d', src]) if ret != 0: @@ -559,7 +560,7 @@ def gpg_decrypt_file(src, dst, keypass): def gpg_verify_file(src, dst, signer=None): src = path_for_gpg(src) dst = path_for_gpg(dst) - ret, out, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, '--batch', + ret, _, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, '--batch', '--yes', '--trust-model', 'always', '-o', dst, '--verify', src]) if ret != 0: raise_err('gpg verification failed', err) @@ -1182,15 +1183,15 @@ def test_generate_to_kbx(self): pipe = pswd_pipe(PASSWORD) kbx_userid_tracker = 'kbx_userid_tracker@rnp' # Run key generation - ret, out, err = run_proc(RNPK, ['--gen-key', '--keystore-format', 'GPG21', + ret, out, _ = run_proc(RNPK, ['--gen-key', '--keystore-format', 'GPG21', '--userid', kbx_userid_tracker, '--homedir', RNPDIR, '--pass-fd', str(pipe)]) os.close(pipe) self.assertEqual(ret, 0, KEY_GEN_FAILED) # Read KBX with GPG - ret, out, err = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys']) + ret, out, _ = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys']) self.assertEqual(ret, 0, 'gpg : failed to read KBX') - self.assertTrue(kbx_userid_tracker in out, 'gpg : failed to read expected key from KBX') + self.assertIn(kbx_userid_tracker, out, 'gpg : failed to read expected key from KBX') clear_keyrings() def test_generate_protection_pass_fd(self): @@ -1438,46 +1439,47 @@ def test_export_revocation(self): def test_import_keys(self): clear_keyrings() + KEY_BOTH=data_path('test_stream_key_merge/key-both.asc') # try to import non-existing file - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('thiskeyfiledoesnotexist')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('thiskeyfiledoesnotexist')]) self.assertEqual(ret, 1) self.assertRegex(err, r'(?s)^.*Failed to create input for .*thiskeyfiledoesnotexist.*') # try malformed file - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_key_validity/alice-sigs-malf.pgp')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_key_validity/alice-sigs-malf.pgp')]) self.assertEqual(ret, 1) self.assertRegex(err, r'(?s)^.*failed to import key\(s\) from .*test_key_validity/alice-sigs-malf.pgp, stopping\..*') self.assertRegex(err, r'(?s)^.*Import finished: 0 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 0 unchanged\..*') # try --import - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 2 new public keys, 0 new secret keys, 0 updated, 0 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 2 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_stream_key_merge/key-both.asc')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', KEY_BOTH]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 3 new public keys, 3 new secret keys, 0 updated, 0 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_stream_key_merge/key-both.asc')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', KEY_BOTH]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 6 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-sign-sub-exp-pub.asc')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-sign-sub-exp-pub.asc')]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 1 new public keys, 0 new secret keys, 1 updated, 0 unchanged\..*') clear_keyrings() # try --import-key - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path(KEY_ALICE_SUB_PUB)]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path(KEY_ALICE_SUB_PUB)]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 2 new public keys, 0 new secret keys, 0 updated, 0 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path(KEY_ALICE_SUB_PUB)]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path(KEY_ALICE_SUB_PUB)]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 2 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_stream_key_merge/key-both.asc')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', KEY_BOTH]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 3 new public keys, 3 new secret keys, 0 updated, 0 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_stream_key_merge/key-both.asc')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', KEY_BOTH]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 6 unchanged\..*') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_key_validity/alice-sign-sub-exp-pub.asc')]) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_key_validity/alice-sign-sub-exp-pub.asc')]) self.assertEqual(ret, 0) self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 1 new public keys, 0 new secret keys, 1 updated, 0 unchanged\..*') clear_keyrings() @@ -1700,13 +1702,13 @@ def _test_userid_genkey(self, userid_beginning, weird_part, userid_end, weird_pa for userid in USERS: rnp_genkey_rsa(userid, 1024) # Read with GPG - ret, out, err = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys', '--charset', CONSOLE_ENCODING]) + ret, out, _ = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys', '--charset', CONSOLE_ENCODING]) self.assertEqual(ret, 0, 'gpg : failed to read keystore') tracker_escaped = re.findall(r'' + userid_beginning + '.*' + userid_end + '', out) tracker_gpg = list(map(decode_string_escape, tracker_escaped)) self.assertEqual(tracker_gpg, USERS, 'gpg : failed to find expected userids from keystore') # Read with rnpkeys - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) self.assertEqual(ret, 0, 'rnpkeys : failed to read keystore') tracker_escaped = re.findall(r'' + userid_beginning + '.*' + userid_end + '', out) tracker_rnp = list(map(decode_string_escape, tracker_escaped)) @@ -1820,31 +1822,33 @@ def test_additional_subkeys_default(self): clear_keyrings() def test_expert_mode_no_endless_loop(self): - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, - '--userid', 'noendlessloop@rnp', '--expert', '--generate-key'], + TOO_MANY=r'(?s)Too many attempts. Aborting.' + UID='noendlessloop@rnp' + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', UID, '--expert', '--generate-key'], '\n\n\n\n\n') self.assertEqual(ret, 1) - self.assertRegex(out, r'(?s)Too many attempts. Aborting.') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, - '--userid', 'noendlessloop@rnp', '--expert', '--generate-key'], + self.assertRegex(out, TOO_MANY) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', UID, '--expert', '--generate-key'], '1\n1\n1\n1\n1\n1\n') self.assertEqual(ret, 1) - self.assertRegex(out, r'(?s)Too many attempts. Aborting.') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, - '--userid', 'noendlessloop@rnp', '--expert', '--generate-key'], + self.assertRegex(out, TOO_MANY) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', UID, '--expert', '--generate-key'], '16\n1\n1\n1\n1\n1\n') self.assertEqual(ret, 1) - self.assertRegex(out, r'(?s)Too many attempts. Aborting.') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, - '--userid', 'noendlessloop@rnp', '--expert', '--generate-key'], + self.assertRegex(out, TOO_MANY) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', UID, '--expert', '--generate-key'], '17\n1\n1\n1\n1\n1\n') self.assertEqual(ret, 1) - self.assertRegex(out, r'(?s)Too many attempts. Aborting.') - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, - '--userid', 'noendlessloop@rnp', '--expert', '--generate-key'], + self.assertRegex(out, TOO_MANY) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', UID, '--expert', '--generate-key'], '19\n\n\n\n\n\n') self.assertEqual(ret, 1) - self.assertRegex(out, r'(?s)Too many attempts. Aborting.') + self.assertRegex(out, TOO_MANY) clear_keyrings() @@ -2239,7 +2243,7 @@ def test_armor(self): def test_rnpkeys_lists(self): KEYRING_1 = data_path(KEYRING_DIR_1) - KEYRING_2 = data_path('keyrings/2') + KEYRING_2 = data_path(KEYRING_DIR_2) KEYRING_3 = data_path(KEYRING_DIR_3) KEYRING_5 = data_path('keyrings/5') path = data_path('test_cli_rnpkeys') + '/' @@ -2337,7 +2341,7 @@ def test_rnpkeys_g10_list_order(self): self.assertEqual(file_text(data_path('test_cli_rnpkeys/g10_list_keys_sec_no_bp')), out, 'g10 secret key listing failed') def test_rnpkeys_list_from_keyfile(self): - KEYRING_2 = data_path('keyrings/2') + KEYRING_2 = data_path(KEYRING_DIR_2) ret, out, err = run_proc(RNPK, ['--homedir', KEYRING_2, '--list-keys', '--keyfile', data_path(KEY_ALICE_PUB)]) self.assertEqual(ret, 0) self.assertRegex(out, r'1 key found.*') @@ -2669,7 +2673,7 @@ def test_rnp_list_packets_edge_cases(self): def test_debug_log(self): if RNP_CAST5: run_proc(RNPK, ['--homedir', data_path(KEYRING_DIR_1), '--list-keys', '--debug', '--all']) - run_proc(RNPK, ['--homedir', data_path('keyrings/2'), '--list-keys', '--debug', '--all']) + run_proc(RNPK, ['--homedir', data_path(KEYRING_DIR_2), '--list-keys', '--debug', '--all']) run_proc(RNPK, ['--homedir', data_path(KEYRING_DIR_3), '--list-keys', '--debug', '--all']) run_proc(RNPK, ['--homedir', data_path(SECRING_G10), '--list-keys', '--debug', '--all']) @@ -2851,55 +2855,55 @@ def test_output_to_specifier(self): def test_literal_filename(self): EMPTY_FNAME = r'(?s)^.*literal data packet.*mode b.*created 0, name="".*$' HELLO_FNAME = r'(?s)^.*literal data packet.*mode b.*created 0, name="hello".*$' - src, enc, dec = reg_workfiles('source', '.txt', EXT_PGP, '.dec') + src, enc = reg_workfiles('source', '.txt', EXT_PGP) with open(src, 'w+') as f: f.write('Literal filename check') # Encrypt file and make sure it's name is stored in literal data packet ret, out, _ = run_proc(RNP, ['-c', src, '--password', 'password']) self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, r'(?s)^.*literal data packet.*mode b.*created \d+.*name="source.txt".*$') remove_files(enc) # Encrypt file, overriding it's name ret, out, _ = run_proc(RNP, ['--set-filename', 'hello', '-c', src, '--password', 'password']) self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, HELLO_FNAME) remove_files(enc) # Encrypt file, using empty name ret, out, _ = run_proc(RNP, ['--set-filename', '', '-c', src, '--password', 'password']) self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, EMPTY_FNAME) remove_files(enc) # Encrypt stdin, making sure empty name is stored ret, out, _ = run_proc(RNP, ['-c', '--password', 'password', '--output', enc], 'Data from stdin') self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, EMPTY_FNAME) remove_files(enc) # Encrypt stdin, setting the file name ret, out, _ = run_proc(RNP, ['--set-filename', 'hello', '-c', '--password', 'password', '--output', enc], 'Data from stdin') self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, HELLO_FNAME) remove_files(enc) # Encrypt env, making sure empty name is stored ret, out, _ = run_proc(RNP, ['-c', 'env:HOME', '--password', 'password', '--output', enc]) self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, EMPTY_FNAME) remove_files(enc) # Encrypt env, setting the file name ret, out, _ = run_proc(RNP, ['--set-filename', 'hello', '-c', 'env:HOME', '--password', 'password', '--output', enc]) self.assertEqual(ret, 0) - ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + ret, out, _ = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, HELLO_FNAME) remove_files(enc) @@ -3692,15 +3696,15 @@ def test_ext_adding_stripping(self): def test_interactive_password(self): # Reuse password for subkey, say "yes" stdinstr = 'password\npassword\ny\n' - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) self.assertEqual(ret, 0) # Do not reuse same password for subkey, say "no" stdinstr = 'password\npassword\nN\nsubkeypassword\nsubkeypassword\n' - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) self.assertEqual(ret, 0) # Set empty password and reuse it stdinstr = '\n\ny\ny\n' - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) self.assertEqual(ret, 0) def test_set_current_time(self): @@ -3779,7 +3783,7 @@ def test_keystore_formats(self): def test_no_twofish(self): if (RNP_TWOFISH): - return + self.skipTest('Twofish is available') src, dst, dec = reg_workfiles('cleartext', '.txt', '.pgp', '.dec') random_text(src, 100) # Attempt to encrypt to twofish @@ -3809,7 +3813,7 @@ def test_no_twofish(self): def test_no_idea(self): if (RNP_IDEA): - return + self.skipTest('IDEA is available') src, dst, dec = reg_workfiles('cleartext', '.txt', '.pgp', '.dec') random_text(src, 100) # Attempt to encrypt to twofish @@ -4206,7 +4210,7 @@ def s2k_msec_iters(msec): ret, out, _ = run_proc(RNP, ['--list-packets', enc]) self.assertEqual(ret, 0) self.assertRegex(out, r'(?s)^.*s2k iterations: [0-9]+ \(encoded as [0-9]+\).*') - matches = re.findall(r'(?s)^.*s2k iterations: ([0-9]+) \(encoded as [0-9]+\).*', out) + matches = re.findall(r'(?s)^.*s2k iterations: (\d+) \(encoded as \d+\).*', out) remove_files(enc) return int(matches[0]) @@ -4250,8 +4254,7 @@ def test_sym_encryption_wrong_s2k(self): def test_sym_encryption__rnp_aead(self): if not RNP_AEAD: - print('AEAD is not available for RNP - skipping.') - return + self.skipTest('AEAD is not available for RNP - skipping.') CIPHERS = rnp_supported_ciphers(True) AEADS = [None, 'eax', 'ocb'] if not RNP_AEAD_EAX: From 3224ebf5c8aed59f1cb7e0921a049d08ade0ab12 Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Tue, 18 Jun 2024 19:22:08 +0300 Subject: [PATCH 7/8] Update FindJSON-C cmake module to correctly support json-c12 and json-c13 naming scheme. --- cmake/Modules/FindJSON-C.cmake | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmake/Modules/FindJSON-C.cmake b/cmake/Modules/FindJSON-C.cmake index e66a011657..cdb8d6be55 100644 --- a/cmake/Modules/FindJSON-C.cmake +++ b/cmake/Modules/FindJSON-C.cmake @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ribose Inc. +# Copyright (c) 2018, 2024 Ribose Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -58,18 +58,23 @@ if (NOT PC_JSON-C_FOUND) pkg_check_modules(PC_JSON-C QUIET json-c12) endif() +# ..or even json-c13, accompanied by non-develop json-c (RHEL 8 ubi) +if (NOT PC_JSON-C_FOUND) + pkg_check_modules(PC_JSON-C QUIET json-c13) +endif() + # find the headers find_path(JSON-C_INCLUDE_DIR NAMES json_c_version.h HINTS ${PC_JSON-C_INCLUDEDIR} ${PC_JSON-C_INCLUDE_DIRS} - PATH_SUFFIXES json-c + PATH_SUFFIXES json-c json-c12 json-c13 ) # find the library find_library(JSON-C_LIBRARY - NAMES json-c libjson-c json-c12 libjson-c12 + NAMES json-c libjson-c json-c12 libjson-c12 json-c13 libjson-c13 HINTS ${PC_JSON-C_LIBDIR} ${PC_JSON-C_LIBRARY_DIRS} @@ -120,4 +125,3 @@ if (JSON-C_FOUND AND NOT TARGET JSON-C::JSON-C) endif() mark_as_advanced(JSON-C_INCLUDE_DIR JSON-C_LIBRARY) - From 8eae86f45810a0156c28616c24ea3a86feada112 Mon Sep 17 00:00:00 2001 From: Nickolay Olshevsky Date: Tue, 18 Jun 2024 19:25:34 +0300 Subject: [PATCH 8/8] CI: add RHEL 8 and 9 runners --- .github/workflows/centos-and-fedora.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/centos-and-fedora.yml b/.github/workflows/centos-and-fedora.yml index cdc7248e28..abd34a803c 100644 --- a/.github/workflows/centos-and-fedora.yml +++ b/.github/workflows/centos-and-fedora.yml @@ -68,9 +68,10 @@ jobs: - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan', botan_ver: 'head', gpg_ver: 'system' } - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'Botan', botan_ver: '3.3.0', pqc: On, gpg_ver: 'system' } - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'OpenSSL', gpg_ver: 'lts' } - - { name: 'CentOS 9', container: 'centos-9-amd64', backend: 'OpenSSL', idea: Off,gpg_ver: 'stable' } - { name: 'Fedora 39', container: 'fedora-39-amd64', backend: 'OpenSSL', gpg_ver: 'system' } - { name: 'Fedora 40', container: 'fedora-40-amd64', backend: 'OpenSSL', gpg_ver: 'system' } + - { name: 'RHEL 8', container: 'redhat-8-ubi', backend: 'OpenSSL', gpg_ver: 'system' } + - { name: 'RHEL 9', container: 'redhat-9-ubi', backend: 'OpenSSL', gpg_ver: 'system' } # There is some ABI incompatibility between llvm-7, bitan shared library from ribose repo and sanitizer # So we are enforving static lib for sanitizers on CentOS 7 @@ -95,6 +96,9 @@ jobs: # Coverage report for Botan backend with disabled algos - image: { name: 'Fedora 40 Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: Botan, idea: Off, sm2: Off, two: Off, blow: Off, rmd: Off, bp: Off } env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } + # Coverage report for OpenSSL 1.1.1 backend within RHEL 8 + - image: { name: 'RHEL 8 Coverage', container: 'redhat-8-ubi', gpg_ver: stable, backend: OpenSSL } + env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: on } # Coverage report for PQC - not running yet due to very low coverage #- image: { name: 'Fedora 40 PQC Coverage', container: 'fedora-40-amd64', gpg_ver: stable, backend: Botan, botan_ver: '3.3.0', pqc: On } # env: { CC: gcc, CXX: g++, BUILD_MODE: coverage, SHARED_LIBS: off }