From 4deafc1ef382220c07e161a82f8afda0de65753e Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter <10532077+lubux@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:59:27 +0100 Subject: [PATCH] Crypto refresh support and V2 API (#182) This squashed commit adds complete support for the OpenPGP crypto refresh. Additionally, the commit introduces an enhanced, non-backwards compatible v2 API. The API in the openpgp package remains fully backward compatible, while the new v2 API is located in a separate v2 package inside the openpgp folder. --- .github/actions/build-gosop/action.yml | 13 +- .github/test-suite/build_gosop.sh | 2 +- .github/test-suite/build_gosop_v1.sh | 5 + .github/test-suite/config.json.template | 8 +- .github/test-suite/prepare_config.sh | 8 +- .github/workflows/go.yml | 4 +- .github/workflows/interop-test-suite.yml | 86 +- .gitignore | 3 +- README.md | 2 + go.mod | 8 +- go.sum | 28 +- ocb/ocb.go | 5 +- openpgp/armor/armor.go | 66 +- openpgp/armor/armor_test.go | 4 +- openpgp/armor/encode.go | 77 +- openpgp/canonical_text.go | 12 +- openpgp/clearsign/clearsign.go | 168 +- openpgp/clearsign/clearsign_test.go | 137 +- openpgp/ed25519/ed25519.go | 115 + openpgp/ed25519/ed25519_test.go | 66 + openpgp/ed448/ed448.go | 119 + openpgp/ed448/ed448_test.go | 63 + openpgp/errors/errors.go | 29 +- openpgp/integration_tests/end_to_end_test.go | 24 +- openpgp/integration_tests/utils_test.go | 27 +- .../integration_tests/v2/end_to_end_test.go | 400 ++++ .../v2/testdata/test_vectors.json | 98 + openpgp/integration_tests/v2/utils_test.go | 306 +++ openpgp/internal/algorithm/cipher.go | 12 +- openpgp/internal/ecc/curve_info.go | 3 +- openpgp/key_generation.go | 106 +- openpgp/keys.go | 125 +- openpgp/keys_test.go | 73 +- openpgp/keys_v5_test.go | 105 - openpgp/keys_v6_test.go | 198 ++ openpgp/packet/aead_crypter.go | 29 +- openpgp/packet/aead_encrypted_test.go | 18 +- openpgp/packet/compressed.go | 45 +- openpgp/packet/compressed_test.go | 8 +- openpgp/packet/config.go | 110 +- openpgp/packet/encrypted_key.go | 413 +++- openpgp/packet/encrypted_key_test.go | 116 +- openpgp/packet/literal.go | 6 +- openpgp/packet/marker.go | 33 + openpgp/packet/one_pass_signature.go | 132 +- openpgp/packet/opaque.go | 3 +- openpgp/packet/packet.go | 154 +- openpgp/packet/packet_sequence.go | 222 ++ openpgp/packet/packet_test.go | 5 +- openpgp/packet/packet_unsupported.go | 24 + openpgp/packet/padding.go | 27 + openpgp/packet/private_key.go | 522 ++++- openpgp/packet/private_key_test.go | 144 +- openpgp/packet/public_key.go | 397 +++- openpgp/packet/public_key_test.go | 2 +- openpgp/packet/reader.go | 159 +- openpgp/packet/recipient.go | 15 + openpgp/packet/signature.go | 405 +++- openpgp/packet/signature_test.go | 2 +- openpgp/packet/symmetric_key_encrypted.go | 71 +- .../symmetric_key_encrypted_data_test.go | 9 +- .../packet/symmetric_key_encrypted_test.go | 16 +- .../packet/symmetrically_encrypted_aead.go | 2 +- openpgp/packet/symmetrically_encrypted_mdc.go | 5 +- .../packet/symmetrically_encrypted_test.go | 12 +- openpgp/packet/userattribute.go | 3 +- openpgp/packet/userid.go | 3 +- openpgp/read.go | 118 +- openpgp/read_test.go | 136 +- openpgp/read_write_test_data.go | 201 +- openpgp/s2k/s2k.go | 15 +- openpgp/s2k/s2k_cache.go | 2 +- openpgp/s2k/s2k_config.go | 6 +- openpgp/s2k/s2k_test.go | 15 +- openpgp/v2/canonical_text.go | 117 + openpgp/v2/canonical_text_test.go | 52 + openpgp/v2/hash.go | 24 + openpgp/v2/key_generation.go | 524 +++++ openpgp/v2/keys.go | 786 +++++++ openpgp/v2/keys_test.go | 2013 +++++++++++++++++ openpgp/v2/keys_test_data.go | 538 +++++ openpgp/v2/keys_v5_test.go | 95 + openpgp/v2/keys_v6_test.go | 226 ++ openpgp/v2/read.go | 800 +++++++ openpgp/v2/read_test.go | 1002 ++++++++ openpgp/v2/read_write_test_data.go | 742 ++++++ openpgp/v2/subkeys.go | 207 ++ openpgp/v2/user.go | 217 ++ openpgp/v2/write.go | 1053 +++++++++ openpgp/v2/write_test.go | 994 ++++++++ openpgp/write.go | 57 +- openpgp/write_test.go | 45 +- openpgp/x25519/x25519.go | 221 ++ openpgp/x25519/x25519_test.go | 59 + openpgp/x448/x448.go | 229 ++ openpgp/x448/x448_test.go | 59 + 96 files changed, 15160 insertions(+), 1010 deletions(-) create mode 100755 .github/test-suite/build_gosop_v1.sh create mode 100644 openpgp/ed25519/ed25519.go create mode 100644 openpgp/ed25519/ed25519_test.go create mode 100644 openpgp/ed448/ed448.go create mode 100644 openpgp/ed448/ed448_test.go create mode 100644 openpgp/integration_tests/v2/end_to_end_test.go create mode 100644 openpgp/integration_tests/v2/testdata/test_vectors.json create mode 100644 openpgp/integration_tests/v2/utils_test.go create mode 100644 openpgp/keys_v6_test.go create mode 100644 openpgp/packet/marker.go create mode 100644 openpgp/packet/packet_sequence.go create mode 100644 openpgp/packet/packet_unsupported.go create mode 100644 openpgp/packet/padding.go create mode 100644 openpgp/packet/recipient.go create mode 100644 openpgp/v2/canonical_text.go create mode 100644 openpgp/v2/canonical_text_test.go create mode 100644 openpgp/v2/hash.go create mode 100644 openpgp/v2/key_generation.go create mode 100644 openpgp/v2/keys.go create mode 100644 openpgp/v2/keys_test.go create mode 100644 openpgp/v2/keys_test_data.go create mode 100644 openpgp/v2/keys_v5_test.go create mode 100644 openpgp/v2/keys_v6_test.go create mode 100644 openpgp/v2/read.go create mode 100644 openpgp/v2/read_test.go create mode 100644 openpgp/v2/read_write_test_data.go create mode 100644 openpgp/v2/subkeys.go create mode 100644 openpgp/v2/user.go create mode 100644 openpgp/v2/write.go create mode 100644 openpgp/v2/write_test.go create mode 100644 openpgp/x25519/x25519.go create mode 100644 openpgp/x25519/x25519_test.go create mode 100644 openpgp/x448/x448.go create mode 100644 openpgp/x448/x448_test.go diff --git a/.github/actions/build-gosop/action.yml b/.github/actions/build-gosop/action.yml index 5f4fa7639..a1e651b77 100644 --- a/.github/actions/build-gosop/action.yml +++ b/.github/actions/build-gosop/action.yml @@ -13,6 +13,16 @@ inputs: required: true default: './gosop-${{ github.sha }}' + branch-gosop: + description: 'Branch of the gosop to use' + required: false + default: 'main' + + gosop-build-path: + description: 'Build script of the gosop to use' + required: false + default: 'build_gosop_v1.sh' + runs: using: "composite" steps: @@ -30,6 +40,7 @@ runs: uses: actions/checkout@v3 with: repository: ProtonMail/gosop + ref: ${{ inputs.branch-gosop }} path: gosop - name: Cache go modules uses: actions/cache@v3 @@ -41,7 +52,7 @@ runs: restore-keys: | ${{ runner.os }}-go- - name: Build gosop - run: ./.github/test-suite/build_gosop.sh + run: ./.github/test-suite/${{ inputs.gosop-build-path }} shell: bash # Test the binary - name: Print gosop version diff --git a/.github/test-suite/build_gosop.sh b/.github/test-suite/build_gosop.sh index e528d36bc..fc627d71e 100755 --- a/.github/test-suite/build_gosop.sh +++ b/.github/test-suite/build_gosop.sh @@ -1,5 +1,5 @@ cd gosop echo "replace github.com/ProtonMail/go-crypto => ../go-crypto" >> go.mod go get github.com/ProtonMail/go-crypto -go get github.com/ProtonMail/gopenpgp/v2/crypto@latest +go get github.com/ProtonMail/gopenpgp/v3/crypto@8acccb3915b46d8765d536ff9669bb61ec567f77 go build . diff --git a/.github/test-suite/build_gosop_v1.sh b/.github/test-suite/build_gosop_v1.sh new file mode 100755 index 000000000..e528d36bc --- /dev/null +++ b/.github/test-suite/build_gosop_v1.sh @@ -0,0 +1,5 @@ +cd gosop +echo "replace github.com/ProtonMail/go-crypto => ../go-crypto" >> go.mod +go get github.com/ProtonMail/go-crypto +go get github.com/ProtonMail/gopenpgp/v2/crypto@latest +go build . diff --git a/.github/test-suite/config.json.template b/.github/test-suite/config.json.template index ac3701f34..60529ac83 100644 --- a/.github/test-suite/config.json.template +++ b/.github/test-suite/config.json.template @@ -1,8 +1,12 @@ { "drivers": [ { - "id": "gosop-branch", - "path": "__GOSOP_BRANCH__" + "id": "gosop-branch-v1", + "path": "__GOSOP_BRANCH_V1__" + }, + { + "id": "gosop-branch-v2", + "path": "__GOSOP_BRANCH_V2__" }, { "id": "gosop-main", diff --git a/.github/test-suite/prepare_config.sh b/.github/test-suite/prepare_config.sh index ae49d01fc..cc5883ccf 100755 --- a/.github/test-suite/prepare_config.sh +++ b/.github/test-suite/prepare_config.sh @@ -1,9 +1,11 @@ CONFIG_TEMPLATE=$1 CONFIG_OUTPUT=$2 -GOSOP_BRANCH=$3 -GOSOP_MAIN=$4 +GOSOP_BRANCH_V1=$3 +GOSOP_BRANCH_V2=$4 +GOSOP_MAIN=$5 cat $CONFIG_TEMPLATE \ - | sed "s@__GOSOP_BRANCH__@${GOSOP_BRANCH}@g" \ + | sed "s@__GOSOP_BRANCH_V1__@${GOSOP_BRANCH_V1}@g" \ + | sed "s@__GOSOP_BRANCH_V2__@${GOSOP_BRANCH_V2}@g" \ | sed "s@__GOSOP_MAIN__@${GOSOP_MAIN}@g" \ | sed "s@__SQOP__@${SQOP}@g" \ | sed "s@__GPGME_SOP__@${GPGME_SOP}@g" \ diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 48ced7ab8..1a05c9196 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -32,10 +32,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Go 1.15 + - name: Set up Go 1.17 uses: actions/setup-go@v3 with: - go-version: 1.15 + go-version: 1.17 - name: Short test run: go test -short -v ./... diff --git a/.github/workflows/interop-test-suite.yml b/.github/workflows/interop-test-suite.yml index 9d6fb0d20..83b8d01d3 100644 --- a/.github/workflows/interop-test-suite.yml +++ b/.github/workflows/interop-test-suite.yml @@ -5,9 +5,25 @@ on: branches: [ main ] jobs: - - build-gosop: - name: Build gosop from branch + build-gosop-v1: + name: Build gosop from branch v1-api + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build gosop from branch + uses: ./.github/actions/build-gosop + with: + binary-location: ./gosop-${{ github.sha }}-v1 + # Upload as artifact + - name: Upload gosop artifact + uses: actions/upload-artifact@v3 + with: + name: gosop-${{ github.sha }}-v1 + path: ./gosop-${{ github.sha }}-v1 + + build-gosop-v2: + name: Build gosop from branch v2-api runs-on: ubuntu-latest steps: - name: Checkout @@ -15,13 +31,15 @@ jobs: - name: Build gosop from branch uses: ./.github/actions/build-gosop with: - binary-location: ./gosop-${{ github.sha }} + binary-location: ./gosop-${{ github.sha }}-v2 + branch-gosop: gosop-gopenpgp-v3 + gosop-build-path: build_gosop.sh # Upload as artifact - name: Upload gosop artifact uses: actions/upload-artifact@v3 with: - name: gosop-${{ github.sha }} - path: ./gosop-${{ github.sha }} + name: gosop-${{ github.sha }}-v2 + path: ./gosop-${{ github.sha }}-v2 build-gosop-main: name: Build gosop from main @@ -40,18 +58,18 @@ jobs: with: name: gosop-main path: ./gosop-main - test-suite: name: Run interoperability test suite runs-on: ubuntu-latest container: - image: ghcr.io/protonmail/openpgp-interop-test-docker:v1.1.1 + image: ghcr.io/protonmail/openpgp-interop-test-docker:v.1.1.3 credentials: username: ${{ github.actor }} password: ${{ secrets.github_token }} needs: - - build-gosop + - build-gosop-v1 + - build-gosop-v2 - build-gosop-main steps: - name: Checkout @@ -66,21 +84,33 @@ jobs: run: chmod +x gosop-main - name: Print gosop-main version run: ./gosop-main version --extended - # Fetch gosop from branch - - name: Download gosop-branch + # Fetch gosop from branch v1 + - name: Download gosop-branch-v1 + uses: actions/download-artifact@v3 + with: + name: gosop-${{ github.sha }}-v1 + - name: Rename gosop-branch-v1 + run: mv gosop-${{ github.sha }}-v1 gosop-branch-v1 + # Test gosop-branch v1 + - name: Make gosop-branch-v1 executable + run: chmod +x gosop-branch-v1 + - name: Print gosop-branch-v1 version + run: ./gosop-branch-v1 version --extended + # Fetch gosop from branch v2 + - name: Download gosop-branch-v2 uses: actions/download-artifact@v3 with: - name: gosop-${{ github.sha }} - - name: Rename gosop-branch - run: mv gosop-${{ github.sha }} gosop-branch - # Test gosop-branch - - name: Make gosop-branch executable - run: chmod +x gosop-branch - - name: Print gosop-branch version - run: ./gosop-branch version --extended + name: gosop-${{ github.sha }}-v2 + - name: Rename gosop-branch-v2 + run: mv gosop-${{ github.sha }}-v2 gosop-branch-v2 + # Test gosop-branch v2 + - name: Make gosop-branch-v2 executable + run: chmod +x gosop-branch-v2 + - name: Print gosop-branch-v2 version + run: ./gosop-branch-v2 version --extended # Run test suite - name: Prepare test configuration - run: ./.github/test-suite/prepare_config.sh $CONFIG_TEMPLATE $CONFIG_OUTPUT $GITHUB_WORKSPACE/gosop-branch $GITHUB_WORKSPACE/gosop-main + run: ./.github/test-suite/prepare_config.sh $CONFIG_TEMPLATE $CONFIG_OUTPUT $GITHUB_WORKSPACE/gosop-branch-v1 $GITHUB_WORKSPACE/gosop-branch-v2 $GITHUB_WORKSPACE/gosop-main env: CONFIG_TEMPLATE: .github/test-suite/config.json.template CONFIG_OUTPUT: .github/test-suite/config.json @@ -116,10 +146,18 @@ jobs: uses: actions/download-artifact@v3 with: name: test-suite-results.json - - name: Compare with baseline - uses: ProtonMail/openpgp-interop-test-analyzer@v1 + - name: Compare with baseline v1 + uses: ProtonMail/openpgp-interop-test-analyzer@5d7f4b6868ebe3bfc909302828342c461f5f4940 with: results: ${{ steps.download-test-results.outputs.download-path }}/test-suite-results.json - output: baseline-comparison.json + output: baseline-comparison-v1.json baseline: gosop-main - target: gosop-branch + target: gosop-branch-v1 + - name: Compare with baseline v2 + uses: ProtonMail/openpgp-interop-test-analyzer@5d7f4b6868ebe3bfc909302828342c461f5f4940 + with: + results: ${{ steps.download-test-results.outputs.download-path }}/test-suite-results.json + output: baseline-comparison-v2.json + baseline: gosop-main + target: gosop-branch-v2 + diff --git a/.gitignore b/.gitignore index d6d52e87b..2b695db7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Add no patterns to .gitignore except for files generated by the build. last-change -.idea \ No newline at end of file +.idea +settings.json diff --git a/README.md b/README.md index 0f2a16394..dfbc5bd1c 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,5 @@ so you can simply replace all imports of `golang.org/x/crypto/openpgp` with `github.com/ProtonMail/go-crypto/openpgp`. A partial list of changes is here: https://github.com/ProtonMail/go-crypto/issues/21#issuecomment-492792917. + +For the more extended API for reading and writing OpenPGP messages use `github.com/ProtonMail/go-crypto/openpgp/v2`, but it is not fully backwards compatible with `golang.org/x/crypto/openpgp`. diff --git a/go.mod b/go.mod index 3e49a1371..d417da35c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ module github.com/ProtonMail/go-crypto -go 1.13 +go 1.17 require ( - github.com/cloudflare/circl v1.3.3 - golang.org/x/crypto v0.7.0 + github.com/cloudflare/circl v1.3.7 + golang.org/x/crypto v0.17.0 ) + +require golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index 90f8b274e..712b2d44b 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,18 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -23,22 +21,22 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/ocb/ocb.go b/ocb/ocb.go index 1a6f73502..5022285b4 100644 --- a/ocb/ocb.go +++ b/ocb/ocb.go @@ -18,8 +18,9 @@ import ( "crypto/cipher" "crypto/subtle" "errors" - "github.com/ProtonMail/go-crypto/internal/byteutil" "math/bits" + + "github.com/ProtonMail/go-crypto/internal/byteutil" ) type ocb struct { @@ -153,7 +154,7 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte { truncatedNonce := make([]byte, len(nonce)) copy(truncatedNonce, nonce) truncatedNonce[len(truncatedNonce)-1] &= 192 - Ktop := make([]byte, blockSize) + var Ktop []byte if bytes.Equal(truncatedNonce, o.reusableKtop.noncePrefix) { Ktop = o.reusableKtop.Ktop } else { diff --git a/openpgp/armor/armor.go b/openpgp/armor/armor.go index d7af9141e..e0a677f28 100644 --- a/openpgp/armor/armor.go +++ b/openpgp/armor/armor.go @@ -23,7 +23,7 @@ import ( // Headers // // base64-encoded Bytes -// '=' base64 encoded checksum +// '=' base64 encoded checksum (optional) not checked anymore // -----END Type----- // // where Headers is a possibly empty sequence of Key: Value lines. @@ -40,36 +40,15 @@ type Block struct { var ArmorCorrupt error = errors.StructuralError("armor invalid") -const crc24Init = 0xb704ce -const crc24Poly = 0x1864cfb -const crc24Mask = 0xffffff - -// crc24 calculates the OpenPGP checksum as specified in RFC 4880, section 6.1 -func crc24(crc uint32, d []byte) uint32 { - for _, b := range d { - crc ^= uint32(b) << 16 - for i := 0; i < 8; i++ { - crc <<= 1 - if crc&0x1000000 != 0 { - crc ^= crc24Poly - } - } - } - return crc -} - var armorStart = []byte("-----BEGIN ") var armorEnd = []byte("-----END ") var armorEndOfLine = []byte("-----") -// lineReader wraps a line based reader. It watches for the end of an armor -// block and records the expected CRC value. +// lineReader wraps a line based reader. It watches for the end of an armor block type lineReader struct { - in *bufio.Reader - buf []byte - eof bool - crc uint32 - crcSet bool + in *bufio.Reader + buf []byte + eof bool } func (l *lineReader) Read(p []byte) (n int, err error) { @@ -98,26 +77,9 @@ func (l *lineReader) Read(p []byte) (n int, err error) { if len(line) == 5 && line[0] == '=' { // This is the checksum line - var expectedBytes [3]byte - var m int - m, err = base64.StdEncoding.Decode(expectedBytes[0:], line[1:]) - if m != 3 || err != nil { - return - } - l.crc = uint32(expectedBytes[0])<<16 | - uint32(expectedBytes[1])<<8 | - uint32(expectedBytes[2]) - - line, _, err = l.in.ReadLine() - if err != nil && err != io.EOF { - return - } - if !bytes.HasPrefix(line, armorEnd) { - return 0, ArmorCorrupt - } + // Don't check the checksum l.eof = true - l.crcSet = true return 0, io.EOF } @@ -138,23 +100,14 @@ func (l *lineReader) Read(p []byte) (n int, err error) { return } -// openpgpReader passes Read calls to the underlying base64 decoder, but keeps -// a running CRC of the resulting data and checks the CRC against the value -// found by the lineReader at EOF. +// openpgpReader passes Read calls to the underlying base64 decoder. type openpgpReader struct { - lReader *lineReader - b64Reader io.Reader - currentCRC uint32 + lReader *lineReader + b64Reader io.Reader } func (r *openpgpReader) Read(p []byte) (n int, err error) { n, err = r.b64Reader.Read(p) - r.currentCRC = crc24(r.currentCRC, p[:n]) - - if err == io.EOF && r.lReader.crcSet && r.lReader.crc != uint32(r.currentCRC&crc24Mask) { - return 0, ArmorCorrupt - } - return } @@ -222,7 +175,6 @@ TryNextBlock: } p.lReader.in = r - p.oReader.currentCRC = crc24Init p.oReader.lReader = &p.lReader p.oReader.b64Reader = base64.NewDecoder(base64.StdEncoding, &p.lReader) p.Body = &p.oReader diff --git a/openpgp/armor/armor_test.go b/openpgp/armor/armor_test.go index 595612493..861b35473 100644 --- a/openpgp/armor/armor_test.go +++ b/openpgp/armor/armor_test.go @@ -7,7 +7,7 @@ package armor import ( "bytes" "hash/adler32" - "io/ioutil" + "io" "testing" ) @@ -29,7 +29,7 @@ func TestDecodeEncode(t *testing.T) { t.Errorf("result.Header: got:%#v", result.Header) } - contents, err := ioutil.ReadAll(result.Body) + contents, err := io.ReadAll(result.Body) if err != nil { t.Error(err) } diff --git a/openpgp/armor/encode.go b/openpgp/armor/encode.go index 5b6e16c19..112f98b83 100644 --- a/openpgp/armor/encode.go +++ b/openpgp/armor/encode.go @@ -14,6 +14,23 @@ var blockEnd = []byte("\n=") var newline = []byte("\n") var armorEndOfLineOut = []byte("-----\n") +const crc24Init = 0xb704ce +const crc24Poly = 0x1864cfb + +// crc24 calculates the OpenPGP checksum as specified in RFC 4880, section 6.1 +func crc24(crc uint32, d []byte) uint32 { + for _, b := range d { + crc ^= uint32(b) << 16 + for i := 0; i < 8; i++ { + crc <<= 1 + if crc&0x1000000 != 0 { + crc ^= crc24Poly + } + } + } + return crc +} + // writeSlices writes its arguments to the given Writer. func writeSlices(out io.Writer, slices ...[]byte) (err error) { for _, s := range slices { @@ -99,15 +116,18 @@ func (l *lineBreaker) Close() (err error) { // // encoding -> base64 encoder -> lineBreaker -> out type encoding struct { - out io.Writer - breaker *lineBreaker - b64 io.WriteCloser - crc uint32 - blockType []byte + out io.Writer + breaker *lineBreaker + b64 io.WriteCloser + crc uint32 + crcEnabled bool + blockType []byte } func (e *encoding) Write(data []byte) (n int, err error) { - e.crc = crc24(e.crc, data) + if e.crcEnabled { + e.crc = crc24(e.crc, data) + } return e.b64.Write(data) } @@ -118,20 +138,21 @@ func (e *encoding) Close() (err error) { } e.breaker.Close() - var checksumBytes [3]byte - checksumBytes[0] = byte(e.crc >> 16) - checksumBytes[1] = byte(e.crc >> 8) - checksumBytes[2] = byte(e.crc) + if e.crcEnabled { + var checksumBytes [3]byte + checksumBytes[0] = byte(e.crc >> 16) + checksumBytes[1] = byte(e.crc >> 8) + checksumBytes[2] = byte(e.crc) - var b64ChecksumBytes [4]byte - base64.StdEncoding.Encode(b64ChecksumBytes[:], checksumBytes[:]) + var b64ChecksumBytes [4]byte + base64.StdEncoding.Encode(b64ChecksumBytes[:], checksumBytes[:]) - return writeSlices(e.out, blockEnd, b64ChecksumBytes[:], newline, armorEnd, e.blockType, armorEndOfLine) + return writeSlices(e.out, blockEnd, b64ChecksumBytes[:], newline, armorEnd, e.blockType, armorEndOfLine) + } + return writeSlices(e.out, newline, armorEnd, e.blockType, armorEndOfLine) } -// Encode returns a WriteCloser which will encode the data written to it in -// OpenPGP armor. -func Encode(out io.Writer, blockType string, headers map[string]string) (w io.WriteCloser, err error) { +func encode(out io.Writer, blockType string, headers map[string]string, checksum bool) (w io.WriteCloser, err error) { bType := []byte(blockType) err = writeSlices(out, armorStart, bType, armorEndOfLineOut) if err != nil { @@ -151,11 +172,27 @@ func Encode(out io.Writer, blockType string, headers map[string]string) (w io.Wr } e := &encoding{ - out: out, - breaker: newLineBreaker(out, 64), - crc: crc24Init, - blockType: bType, + out: out, + breaker: newLineBreaker(out, 64), + blockType: bType, + crc: crc24Init, + crcEnabled: checksum, } e.b64 = base64.NewEncoder(base64.StdEncoding, e.breaker) return e, nil } + +// Encode returns a WriteCloser which will encode the data written to it in +// OpenPGP armor. +func Encode(out io.Writer, blockType string, headers map[string]string) (w io.WriteCloser, err error) { + return encode(out, blockType, headers, true) +} + +// EncodeWithChecksumOption returns a WriteCloser which will encode the data written to it in +// OpenPGP armor and provides the option to include a checksum. +// When forming ASCII Armor, the CRC24 footer SHOULD NOT be generated, +// unless interoperability with implementations that require the CRC24 footer +// to be present is a concern. +func EncodeWithChecksumOption(out io.Writer, blockType string, headers map[string]string, doChecksum bool) (w io.WriteCloser, err error) { + return encode(out, blockType, headers, doChecksum) +} diff --git a/openpgp/canonical_text.go b/openpgp/canonical_text.go index a94f6150c..5b40e1375 100644 --- a/openpgp/canonical_text.go +++ b/openpgp/canonical_text.go @@ -30,8 +30,12 @@ func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) { if c == '\r' { *s = 1 } else if c == '\n' { - cw.Write(buf[start:i]) - cw.Write(newline) + if _, err := cw.Write(buf[start:i]); err != nil { + return 0, err + } + if _, err := cw.Write(newline); err != nil { + return 0, err + } start = i + 1 } case 1: @@ -39,7 +43,9 @@ func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) { } } - cw.Write(buf[start:]) + if _, err := cw.Write(buf[start:]); err != nil { + return 0, err + } return len(buf), nil } diff --git a/openpgp/clearsign/clearsign.go b/openpgp/clearsign/clearsign.go index 8f70b3730..33fa8e5ac 100644 --- a/openpgp/clearsign/clearsign.go +++ b/openpgp/clearsign/clearsign.go @@ -7,7 +7,7 @@ // // Clearsigned messages are cryptographically signed, but the contents of the // message are kept in plaintext so that it can be read without special tools. -package clearsign // import "github.com/ProtonMail/go-crypto/openpgp/clearsign" +package clearsign // import "github.com/ProtonMail/go-crypto/v2/openpgp/clearsign" import ( "bufio" @@ -52,6 +52,8 @@ var end = []byte("\n-----END PGP SIGNATURE-----") var crlf = []byte("\r\n") var lf = byte('\n') +const hashHeader string = "Hash" + // getLine returns the first \r\n or \n delineated line from the given byte // array. The line does not include the \r\n or \n. The remainder of the byte // array (also not including the new line bytes) is also returned and this will @@ -109,7 +111,7 @@ func Decode(data []byte) (b *Block, rest []byte) { return nil, data } // An empty line marks the end of the headers. - if line, rest = getLine(rest); len(line) == 0 { + if line, rest = getLine(rest); len(strings.TrimSpace(string(line))) == 0 { break } @@ -127,13 +129,15 @@ func Decode(data []byte) (b *Block, rest []byte) { key, val := string(line[0:i]), string(line[i+1:]) key = strings.TrimSpace(key) - if key != "Hash" { + if key == hashHeader { + for _, val := range strings.Split(val, ",") { + val = strings.TrimSpace(val) + b.Headers.Add(key, val) + } + } else { + // Only "Hash" headers are allowed. return nil, data } - for _, val := range strings.Split(val, ",") { - val = strings.TrimSpace(val) - b.Headers.Add(key, val) - } } firstLine := true @@ -199,10 +203,12 @@ func Decode(data []byte) (b *Block, rest []byte) { // When closed, an armored signature is created and written to complete the // message. type dashEscaper struct { - buffered *bufio.Writer - hashers []hash.Hash // one per key in privateKeys - hashType crypto.Hash - toHash io.Writer // writes to all the hashes in hashers + buffered *bufio.Writer + hashers []hash.Hash // one per key in privateKeys + hashType crypto.Hash + toHash io.Writer // writes to all the hashes in hashers + salts [][]byte // salts for the signatures if v6 + armorHeader map[string]string // Armor headers atBeginningOfLine bool isFirstLine bool @@ -222,7 +228,9 @@ func (d *dashEscaper) Write(data []byte) (n int, err error) { // The final CRLF isn't included in the hash so we have to wait // until this point (the start of the next line) before writing it. if !d.isFirstLine { - d.toHash.Write(crlf) + if _, err = d.toHash.Write(crlf); err != nil { + return + } } d.isFirstLine = false } @@ -243,12 +251,16 @@ func (d *dashEscaper) Write(data []byte) (n int, err error) { if _, err = d.buffered.Write(dashEscape); err != nil { return } - d.toHash.Write(d.byteBuf) + if _, err = d.toHash.Write(d.byteBuf); err != nil { + return + } d.atBeginningOfLine = false } else if b == '\n' { // Nothing to do because we delay writing CRLF to the hash. } else { - d.toHash.Write(d.byteBuf) + if _, err = d.toHash.Write(d.byteBuf); err != nil { + return + } d.atBeginningOfLine = false } if err = d.buffered.WriteByte(b); err != nil { @@ -269,13 +281,17 @@ func (d *dashEscaper) Write(data []byte) (n int, err error) { // Any buffered whitespace wasn't at the end of the line so // we need to write it out. if len(d.whitespace) > 0 { - d.toHash.Write(d.whitespace) + if _, err = d.toHash.Write(d.whitespace); err != nil { + return + } if _, err = d.buffered.Write(d.whitespace); err != nil { return } d.whitespace = d.whitespace[:0] } - d.toHash.Write(d.byteBuf) + if _, err = d.toHash.Write(d.byteBuf); err != nil { + return + } if err = d.buffered.WriteByte(b); err != nil { return } @@ -288,20 +304,27 @@ func (d *dashEscaper) Write(data []byte) (n int, err error) { } func (d *dashEscaper) Close() (err error) { - if !d.atBeginningOfLine { - if err = d.buffered.WriteByte(lf); err != nil { - return + if d.atBeginningOfLine { + if !d.isFirstLine { + if _, err := d.toHash.Write(crlf); err != nil { + return err + } } } + if err = d.buffered.WriteByte(lf); err != nil { + return + } - out, err := armor.Encode(d.buffered, "PGP SIGNATURE", nil) + out, err := armor.EncodeWithChecksumOption(d.buffered, "PGP SIGNATURE", d.armorHeader, false) if err != nil { return } t := d.config.Now() + indexSalt := 0 for i, k := range d.privateKeys { sig := new(packet.Signature) + sig.Version = k.Version sig.SigType = packet.SigTypeText sig.PubKeyAlgo = k.PubKeyAlgo sig.Hash = d.hashType @@ -311,7 +334,12 @@ func (d *dashEscaper) Close() (err error) { sig.Notations = d.config.Notations() sigLifetimeSecs := d.config.SigLifetime() sig.SigLifetimeSecs = &sigLifetimeSecs - + if k.Version == 6 { + if err = sig.SetSalt(d.salts[indexSalt]); err != nil { + return + } + indexSalt++ + } if err = sig.Sign(d.hashers[i], k, d.config); err != nil { return } @@ -331,14 +359,29 @@ func (d *dashEscaper) Close() (err error) { // Encode returns a WriteCloser which will clear-sign a message with privateKey // and write it to w. If config is nil, sensible defaults are used. -func Encode(w io.Writer, privateKey *packet.PrivateKey, config *packet.Config) (plaintext io.WriteCloser, err error) { +func Encode(w io.Writer, privateKey *packet.PrivateKey, config *packet.Config, headers map[string]string) (plaintext io.WriteCloser, err error) { return EncodeMulti(w, []*packet.PrivateKey{privateKey}, config) } +// EncodeWithHeader returns a WriteCloser which will clear-sign a message with privateKey +// and write it to w. If config is nil, sensible defaults are used. +// Additionally provides a headers argument for custom headers. +func EncodeWithHeader(w io.Writer, privateKey *packet.PrivateKey, config *packet.Config, headers map[string]string) (plaintext io.WriteCloser, err error) { + return EncodeMultiWithHeader(w, []*packet.PrivateKey{privateKey}, config, headers) +} + // EncodeMulti returns a WriteCloser which will clear-sign a message with all the // private keys indicated and write it to w. If config is nil, sensible defaults // are used. func EncodeMulti(w io.Writer, privateKeys []*packet.PrivateKey, config *packet.Config) (plaintext io.WriteCloser, err error) { + return EncodeMultiWithHeader(w, privateKeys, config, nil) +} + +// EncodeMultiWithHeader returns a WriteCloser which will clear-sign a message with all the +// private keys indicated and write it to w. If config is nil, sensible defaults +// are used. +// Additionally provides a headers argument for custom headers. +func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config *packet.Config, headers map[string]string) (plaintext io.WriteCloser, err error) { for _, k := range privateKeys { if k.Encrypted { return nil, errors.InvalidArgumentError(fmt.Sprintf("signing key %s is encrypted", k.KeyIdString())) @@ -356,8 +399,21 @@ func EncodeMulti(w io.Writer, privateKeys []*packet.PrivateKey, config *packet.C } var hashers []hash.Hash var ws []io.Writer - for range privateKeys { + var salts [][]byte + for _, sk := range privateKeys { h := hashType.New() + if sk.Version == 6 { + // generate salt + var salt []byte + salt, err = packet.SignatureSaltForHash(hashType, config.Random()) + if err != nil { + return + } + if _, err = h.Write(salt); err != nil { + return + } + salts = append(salts, salt) + } hashers = append(hashers, h) ws = append(ws, h) } @@ -371,24 +427,28 @@ func EncodeMulti(w io.Writer, privateKeys []*packet.PrivateKey, config *packet.C if err = buffered.WriteByte(lf); err != nil { return } - if _, err = buffered.WriteString("Hash: "); err != nil { - return - } - if _, err = buffered.WriteString(name); err != nil { - return - } - if err = buffered.WriteByte(lf); err != nil { - return + // write headers + nonV6 := len(salts) < len(hashers) + // Crypto refresh: Headers SHOULD NOT be emitted + if nonV6 { // Emit header if non v6 signatures are present for compatibility + if _, err = buffered.WriteString(fmt.Sprintf("%s: %s", hashHeader, name)); err != nil { + return + } + if err = buffered.WriteByte(lf); err != nil { + return + } } if err = buffered.WriteByte(lf); err != nil { return } plaintext = &dashEscaper{ - buffered: buffered, - hashers: hashers, - hashType: hashType, - toHash: toHash, + buffered: buffered, + hashers: hashers, + hashType: hashType, + toHash: toHash, + salts: salts, + armorHeader: headers, atBeginningOfLine: true, isFirstLine: true, @@ -405,20 +465,8 @@ func EncodeMulti(w io.Writer, privateKeys []*packet.PrivateKey, config *packet.C // VerifySignature checks a clearsigned message signature, and checks that the // hash algorithm in the header matches the hash algorithm in the signature. func (b *Block) VerifySignature(keyring openpgp.KeyRing, config *packet.Config) (signer *openpgp.Entity, err error) { - var expectedHashes []crypto.Hash - for _, v := range b.Headers { - for _, name := range v { - expectedHash := nameToHash(name) - if uint8(expectedHash) == 0 { - return nil, errors.StructuralError("unknown hash algorithm in cleartext message headers") - } - expectedHashes = append(expectedHashes, expectedHash) - } - } - if len(expectedHashes) == 0 { - expectedHashes = append(expectedHashes, crypto.MD5) - } - return openpgp.CheckDetachedSignatureAndHash(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body, expectedHashes, config) + _, signer, err = openpgp.VerifyDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body, config) + return } // nameOfHash returns the OpenPGP name for the given hash, or the empty string @@ -440,25 +488,3 @@ func nameOfHash(h crypto.Hash) string { } return "" } - -// nameToHash returns a hash for a given OpenPGP name, or 0 -// if the name isn't known. See RFC 4880, section 9.4. -func nameToHash(h string) crypto.Hash { - switch h { - case "SHA1": - return crypto.SHA1 - case "SHA224": - return crypto.SHA224 - case "SHA256": - return crypto.SHA256 - case "SHA384": - return crypto.SHA384 - case "SHA512": - return crypto.SHA512 - case "SHA3-256": - return crypto.SHA3_256 - case "SHA3-512": - return crypto.SHA3_512 - } - return crypto.Hash(0) -} diff --git a/openpgp/clearsign/clearsign_test.go b/openpgp/clearsign/clearsign_test.go index ffd5660f3..5d0396be4 100644 --- a/openpgp/clearsign/clearsign_test.go +++ b/openpgp/clearsign/clearsign_test.go @@ -6,13 +6,23 @@ package clearsign import ( "bytes" + "crypto" "fmt" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" "io" + "strings" "testing" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) +var allowAllAlgorithmsConfig = &packet.Config{ + RejectPublicKeyAlgorithms: map[packet.PublicKeyAlgorithm]bool{}, + RejectMessageHashAlgorithms: map[crypto.Hash]bool{}, + RejectCurves: map[packet.Curve]bool{}, + MinRSABits: 512, +} + func testParse(t *testing.T, input []byte, expected, expectedPlaintext string) { b, rest := Decode(input) if b == nil { @@ -37,13 +47,12 @@ func testParse(t *testing.T, input []byte, expected, expectedPlaintext string) { t.Errorf("failed to parse public key: %s", err) } - config := &packet.Config{} - if _, err := openpgp.CheckDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body, config); err != nil { + if _, _, err := openpgp.VerifyDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body, allowAllAlgorithmsConfig); err != nil { t.Errorf("failed to check signature: %s", err) } b, _ = Decode(input) - if _, err := b.VerifySignature(keyring, config); err != nil { + if _, err := b.VerifySignature(keyring, allowAllAlgorithmsConfig); err != nil { t.Errorf("failed to check signature: %s", err) } } @@ -68,21 +77,33 @@ func TestParseWithNoNewlineAtEnd(t *testing.T) { var signingTests = []struct { in, signed, plaintext string }{ - {"", "", ""}, + {"", "", "\n"}, {"a", "a", "a\n"}, - {"a\n", "a", "a\n"}, - {"-a\n", "-a", "-a\n"}, + {"a\n", "a\r\n", "a\n\n"}, + {"-a\n", "-a\r\n", "-a\n\n"}, {"--a\nb", "--a\r\nb", "--a\nb\n"}, // leading whitespace - {" a\n", " a", " a\n"}, - {" a\n", " a", " a\n"}, + {" a\n", " a\r\n", " a\n\n"}, + {" a\n", " a\r\n", " a\n\n"}, // trailing whitespace (should be stripped) - {"a \n", "a", "a\n"}, + {"a \n", "a\r\n", "a\n\n"}, {"a ", "a", "a\n"}, - // whitespace-only lines (should be stripped) - {" \n", "", "\n"}, + {" \n", "\r\n", "\n\n"}, {" ", "", "\n"}, - {"a\n \n \nb\n", "a\r\n\r\n\r\nb", "a\n\n\nb\n"}, + {"a\n \n \nb\n", "a\r\n\r\n\r\nb\r\n", "a\n\n\nb\n\n"}, + {"a\n \n \nb\n", "a\r\n\r\n\r\nb\r\n", "a\n\n\nb\n\n"}, +} + +func TestVerifyV6(t *testing.T) { + keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(clearSignV6PublicKey)) + if err != nil { + t.Errorf("failed to parse public key: %s", err) + } + b, _ := Decode([]byte(clearSignV6)) + _, err = b.VerifySignature(keyring, nil) + if err != nil { + t.Errorf("failed to verify signature: %s", err) + } } func TestSigning(t *testing.T) { @@ -90,11 +111,10 @@ func TestSigning(t *testing.T) { if err != nil { t.Errorf("failed to parse public key: %s", err) } - for i, test := range signingTests { var buf bytes.Buffer - plaintext, err := Encode(&buf, keyring[0].PrivateKey, nil) + plaintext, err := Encode(&buf, keyring[0].PrivateKey, allowAllAlgorithmsConfig, nil) if err != nil { t.Errorf("#%d: error from Encode: %s", i, err) continue @@ -122,13 +142,35 @@ func TestSigning(t *testing.T) { continue } - config := &packet.Config{} - if _, err := openpgp.CheckDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body, config); err != nil { + if _, _, err := openpgp.VerifyDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body, allowAllAlgorithmsConfig); err != nil { t.Errorf("#%d: failed to check signature: %s", i, err) } } } +func TestSigningInterop(t *testing.T) { + keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(signingKey)) + if err != nil { + t.Errorf("failed to parse public key: %s", err) + } + + var buf bytes.Buffer + plaintext, err := Encode(&buf, keyring[0].PrivateKey, nil, nil) + if err != nil { + t.Errorf("error from Encode") + } + if _, err := plaintext.Write([]byte(trickyGrocery)); err != nil { + t.Errorf("error from Write") + } + if err := plaintext.Close(); err != nil { + t.Fatalf("error from Close") + } + expected := "- - tofu\n- - vegetables\n- - noodles\n\n\n" + if !strings.Contains(buf.String(), expected) { + t.Fatalf("expected output to contain %s but got: %s", expected, buf.String()) + } +} + // We use this to make test keys, so that they aren't all the same. type quickRand byte @@ -140,17 +182,17 @@ func (qr *quickRand) Read(p []byte) (int, error) { return len(p), nil } -func TestMultiSign(t *testing.T) { +func testMultiSign(t *testing.T, v6 bool) { if testing.Short() { t.Skip("skipping long test in -short mode") } zero := quickRand(0) - config := packet.Config{Rand: &zero} + config := packet.Config{Rand: &zero, V6Keys: v6} - for nKeys := 0; nKeys < 4; nKeys++ { + for nKeys := 1; nKeys < 4; nKeys++ { nextTest: - for nExtra := 0; nExtra < 4; nExtra++ { + for nExtra := 2; nExtra < 4; nExtra++ { var signKeys []*packet.PrivateKey var verifyKeys openpgp.EntityList @@ -169,7 +211,7 @@ func TestMultiSign(t *testing.T) { input := []byte("this is random text\r\n4 17") var output bytes.Buffer - w, err := EncodeMulti(&output, signKeys, nil) + w, err := EncodeMultiWithHeader(&output, signKeys, nil, nil) if err != nil { t.Errorf("EncodeMulti (%s) failed: %v", desc, err) } @@ -185,7 +227,7 @@ func TestMultiSign(t *testing.T) { t.Errorf("Inline data didn't match original; got %q want %q", string(block.Bytes), string(input)) } config := &packet.Config{} - _, err = openpgp.CheckDetachedSignature(verifyKeys, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body, config) + _, _, err = openpgp.VerifyDetachedSignature(verifyKeys, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body, config) if nKeys == 0 { if err == nil { t.Errorf("verifying inline (%s) succeeded; want failure", desc) @@ -199,6 +241,14 @@ func TestMultiSign(t *testing.T) { } } +func TestMultiSignV4(t *testing.T) { + testMultiSign(t, false) +} + +func TestMultiSignV6(t *testing.T) { + testMultiSign(t, true) +} + func TestDecodeMissingCRC(t *testing.T) { block, rest := Decode(clearsignInput3) if block == nil { @@ -403,3 +453,42 @@ VrM0m72/jnpKo04= =zNCn -----END PGP PRIVATE KEY BLOCK----- ` + +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-08.html#name-sample-v6-certificate-trans +const clearSignV6PublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf +GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy +KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw +gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE +QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn ++eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh +BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8 +j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805 +I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== +-----END PGP PUBLIC KEY BLOCK-----` + +// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/275 +const clearSignV6 = `-----BEGIN PGP SIGNED MESSAGE----- + +What we need from the grocery store: + +- - tofu +- - vegetables +- - noodles + +-----BEGIN PGP SIGNATURE----- + +wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo +/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr +NK2ay45cX1IVAQ== +-----END PGP SIGNATURE-----` + +const trickyGrocery = `From the grocery store we need: + +- tofu +- vegetables +- noodles + +` diff --git a/openpgp/ed25519/ed25519.go b/openpgp/ed25519/ed25519.go new file mode 100644 index 000000000..6abdf7c44 --- /dev/null +++ b/openpgp/ed25519/ed25519.go @@ -0,0 +1,115 @@ +// Package ed25519 implements the ed25519 signature algorithm for OpenPGP +// as defined in the Open PGP crypto refresh. +package ed25519 + +import ( + "crypto/subtle" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + ed25519lib "github.com/cloudflare/circl/sign/ed25519" +) + +const ( + // PublicKeySize is the size, in bytes, of public keys in this package. + PublicKeySize = ed25519lib.PublicKeySize + // SeedSize is the size, in bytes, of private key seeds. + // The private key representation used by RFC 8032. + SeedSize = ed25519lib.SeedSize + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = ed25519lib.SignatureSize +) + +type PublicKey struct { + // Point represents the elliptic curve point of the public key. + Point []byte +} + +type PrivateKey struct { + PublicKey + // Key the private key representation by RFC 8032, + // encoded as seed | pub key point. + Key []byte +} + +// NewPublicKey creates a new empty ed25519 public key. +func NewPublicKey() *PublicKey { + return &PublicKey{} +} + +// NewPrivateKey creates a new empty private key referencing the public key. +func NewPrivateKey(key PublicKey) *PrivateKey { + return &PrivateKey{ + PublicKey: key, + } +} + +// Seed returns the ed25519 private key secret seed. +// The private key representation by RFC 8032. +func (pk *PrivateKey) Seed() []byte { + return pk.Key[:SeedSize] +} + +// MarshalByteSecret returns the underlying 32 byte seed of the private key. +func (pk *PrivateKey) MarshalByteSecret() []byte { + return pk.Seed() +} + +// UnmarshalByteSecret computes the private key from the secret seed +// and stores it in the private key object. +func (sk *PrivateKey) UnmarshalByteSecret(seed []byte) error { + sk.Key = ed25519lib.NewKeyFromSeed(seed) + return nil +} + +// GenerateKey generates a fresh private key with the provided randomness source. +func GenerateKey(rand io.Reader) (*PrivateKey, error) { + publicKey, privateKey, err := ed25519lib.GenerateKey(rand) + if err != nil { + return nil, err + } + privateKeyOut := new(PrivateKey) + privateKeyOut.PublicKey.Point = publicKey[:] + privateKeyOut.Key = privateKey[:] + return privateKeyOut, nil +} + +// Sign signs a message with the ed25519 algorithm. +// priv MUST be a valid key! Check this with Validate() before use. +func Sign(priv *PrivateKey, message []byte) ([]byte, error) { + return ed25519lib.Sign(priv.Key, message), nil +} + +// Verify verifies an ed25519 signature. +func Verify(pub *PublicKey, message []byte, signature []byte) bool { + return ed25519lib.Verify(pub.Point, message, signature) +} + +// Validate checks if the ed25519 private key is valid. +func Validate(priv *PrivateKey) error { + expectedPrivateKey := ed25519lib.NewKeyFromSeed(priv.Seed()) + if subtle.ConstantTimeCompare(priv.Key, expectedPrivateKey) == 0 { + return errors.KeyInvalidError("ed25519: invalid ed25519 secret") + } + if subtle.ConstantTimeCompare(priv.PublicKey.Point, expectedPrivateKey[SeedSize:]) == 0 { + return errors.KeyInvalidError("ed25519: invalid ed25519 public key") + } + return nil +} + +// ENCODING/DECODING signature: + +// WriteSignature encodes and writes an ed25519 signature to writer. +func WriteSignature(writer io.Writer, signature []byte) error { + _, err := writer.Write(signature) + return err +} + +// ReadSignature decodes an ed25519 signature from a reader. +func ReadSignature(reader io.Reader) ([]byte, error) { + signature := make([]byte, SignatureSize) + if _, err := io.ReadFull(reader, signature); err != nil { + return nil, err + } + return signature, nil +} diff --git a/openpgp/ed25519/ed25519_test.go b/openpgp/ed25519/ed25519_test.go new file mode 100644 index 000000000..78f6fc900 --- /dev/null +++ b/openpgp/ed25519/ed25519_test.go @@ -0,0 +1,66 @@ +package ed25519 + +import ( + "crypto/ed25519" + "crypto/rand" + "io" + "testing" +) + +const messageDigestSize = 32 + +func TestGenerate(t *testing.T) { + priv, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + if len(priv.Key) != ed25519.SeedSize+ed25519.PublicKeySize && len(priv.Point) != ed25519.PublicKeySize { + t.Error("generated wrong key sizes") + } +} + +func TestSignVerify(t *testing.T) { + digest := make([]byte, messageDigestSize) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + priv, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + signature, err := Sign(priv, digest) + if err != nil { + t.Errorf("error signing: %s", err) + } + + result := Verify(&priv.PublicKey, digest, signature) + + if !result { + t.Error("unable to verify message") + } + + digest[0] += 1 + result = Verify(&priv.PublicKey, digest, signature) + + if result { + t.Error("signature should be invalid") + } +} + +func TestValidation(t *testing.T) { + priv, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + if err := Validate(priv); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + priv.Key[0] += 1 + if err := Validate(priv); err == nil { + t.Fatal("failed to detect invalid key") + } +} diff --git a/openpgp/ed448/ed448.go b/openpgp/ed448/ed448.go new file mode 100644 index 000000000..b11fb4fb1 --- /dev/null +++ b/openpgp/ed448/ed448.go @@ -0,0 +1,119 @@ +// Package ed448 implements the ed448 signature algorithm for OpenPGP +// as defined in the Open PGP crypto refresh. +package ed448 + +import ( + "crypto/subtle" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + ed448lib "github.com/cloudflare/circl/sign/ed448" +) + +const ( + // PublicKeySize is the size, in bytes, of public keys in this package. + PublicKeySize = ed448lib.PublicKeySize + // SeedSize is the size, in bytes, of private key seeds. + // The private key representation used by RFC 8032. + SeedSize = ed448lib.SeedSize + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = ed448lib.SignatureSize +) + +type PublicKey struct { + // Point represents the elliptic curve point of the public key. + Point []byte +} + +type PrivateKey struct { + PublicKey + // Key the private key representation by RFC 8032, + // encoded as seed | public key point. + Key []byte +} + +// NewPublicKey creates a new empty ed448 public key. +func NewPublicKey() *PublicKey { + return &PublicKey{} +} + +// NewPrivateKey creates a new empty private key referencing the public key. +func NewPrivateKey(key PublicKey) *PrivateKey { + return &PrivateKey{ + PublicKey: key, + } +} + +// Seed returns the ed448 private key secret seed. +// The private key representation by RFC 8032. +func (pk *PrivateKey) Seed() []byte { + return pk.Key[:SeedSize] +} + +// MarshalByteSecret returns the underlying seed of the private key. +func (pk *PrivateKey) MarshalByteSecret() []byte { + return pk.Seed() +} + +// UnmarshalByteSecret computes the private key from the secret seed +// and stores it in the private key object. +func (sk *PrivateKey) UnmarshalByteSecret(seed []byte) error { + sk.Key = ed448lib.NewKeyFromSeed(seed) + return nil +} + +// GenerateKey generates a fresh private key with the provided randomness source. +func GenerateKey(rand io.Reader) (*PrivateKey, error) { + publicKey, privateKey, err := ed448lib.GenerateKey(rand) + if err != nil { + return nil, err + } + privateKeyOut := new(PrivateKey) + privateKeyOut.PublicKey.Point = publicKey[:] + privateKeyOut.Key = privateKey[:] + return privateKeyOut, nil +} + +// Sign signs a message with the ed448 algorithm. +// priv MUST be a valid key! Check this with Validate() before use. +func Sign(priv *PrivateKey, message []byte) ([]byte, error) { + // Ed448 is used with the empty string as a context string. + // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7 + return ed448lib.Sign(priv.Key, message, ""), nil +} + +// Verify verifies a ed448 signature +func Verify(pub *PublicKey, message []byte, signature []byte) bool { + // Ed448 is used with the empty string as a context string. + // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7 + return ed448lib.Verify(pub.Point, message, signature, "") +} + +// Validate checks if the ed448 private key is valid +func Validate(priv *PrivateKey) error { + expectedPrivateKey := ed448lib.NewKeyFromSeed(priv.Seed()) + if subtle.ConstantTimeCompare(priv.Key, expectedPrivateKey) == 0 { + return errors.KeyInvalidError("ed448: invalid ed448 secret") + } + if subtle.ConstantTimeCompare(priv.PublicKey.Point, expectedPrivateKey[SeedSize:]) == 0 { + return errors.KeyInvalidError("ed448: invalid ed448 public key") + } + return nil +} + +// ENCODING/DECODING signature: + +// WriteSignature encodes and writes an ed448 signature to writer. +func WriteSignature(writer io.Writer, signature []byte) error { + _, err := writer.Write(signature) + return err +} + +// ReadSignature decodes an ed448 signature from a reader. +func ReadSignature(reader io.Reader) ([]byte, error) { + signature := make([]byte, SignatureSize) + if _, err := io.ReadFull(reader, signature); err != nil { + return nil, err + } + return signature, nil +} diff --git a/openpgp/ed448/ed448_test.go b/openpgp/ed448/ed448_test.go new file mode 100644 index 000000000..805442c44 --- /dev/null +++ b/openpgp/ed448/ed448_test.go @@ -0,0 +1,63 @@ +package ed448 + +import ( + "crypto/rand" + "io" + "testing" +) + +func TestGenerate(t *testing.T) { + priv, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + if len(priv.Key) != SeedSize+PublicKeySize && len(priv.Point) != PublicKeySize { + t.Error("gnerated wrong key sizes") + } +} + +func TestSignVerify(t *testing.T) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + priv, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + signature, err := Sign(priv, digest) + if err != nil { + t.Errorf("error signing: %s", err) + } + + result := Verify(&priv.PublicKey, digest, signature) + + if !result { + t.Error("unable to verify message") + } + + digest[0] += 1 + result = Verify(&priv.PublicKey, digest, signature) + + if result { + t.Error("signature should be invalid") + } +} + +func TestValidation(t *testing.T) { + priv, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + if err := Validate(priv); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + priv.Key[0] += 1 + if err := Validate(priv); err == nil { + t.Fatal("failed to detect invalid key") + } +} diff --git a/openpgp/errors/errors.go b/openpgp/errors/errors.go index 17e2bcfed..8d6969c0b 100644 --- a/openpgp/errors/errors.go +++ b/openpgp/errors/errors.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package errors contains common error types for the OpenPGP packages. -package errors // import "github.com/ProtonMail/go-crypto/openpgp/errors" +package errors // import "github.com/ProtonMail/go-crypto/v2/openpgp/errors" import ( "strconv" @@ -58,6 +58,14 @@ func (ke keyExpiredError) Error() string { return "openpgp: key expired" } +var ErrSignatureOlderThanKey error = signatureOlderThanKeyError(0) + +type signatureOlderThanKeyError int + +func (ske signatureOlderThanKeyError) Error() string { + return "openpgp: signature is older than the key" +} + var ErrKeyExpired error = keyExpiredError(0) type keyIncorrectError int @@ -92,12 +100,24 @@ func (keyRevokedError) Error() string { var ErrKeyRevoked error = keyRevokedError(0) +type WeakAlgorithmError string + +func (e WeakAlgorithmError) Error() string { + return "openpgp: weak algorithms are rejected: " + string(e) +} + type UnknownPacketTypeError uint8 func (upte UnknownPacketTypeError) Error() string { return "openpgp: unknown packet type: " + strconv.Itoa(int(upte)) } +type CriticalUnknownPacketTypeError uint8 + +func (upte CriticalUnknownPacketTypeError) Error() string { + return "openpgp: unknown critical packet type: " + strconv.Itoa(int(upte)) +} + // AEADError indicates that there is a problem when initializing or using a // AEAD instance, configuration struct, nonces or index values. type AEADError string @@ -114,3 +134,10 @@ type ErrDummyPrivateKey string func (dke ErrDummyPrivateKey) Error() string { return "openpgp: s2k GNU dummy key: " + string(dke) } + +// ErrMalformedMessage results when the packet sequence is incorrect +type ErrMalformedMessage string + +func (dke ErrMalformedMessage) Error() string { + return "openpgp: malformed message " + string(dke) +} diff --git a/openpgp/integration_tests/end_to_end_test.go b/openpgp/integration_tests/end_to_end_test.go index bda40adeb..303cdd612 100644 --- a/openpgp/integration_tests/end_to_end_test.go +++ b/openpgp/integration_tests/end_to_end_test.go @@ -5,15 +5,15 @@ package integrationtests import ( "bytes" "encoding/json" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" "io" - "io/ioutil" "os" "strings" "testing" "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) ///////////////////////////////////////////////////////////////////////////// @@ -41,7 +41,7 @@ func TestEndToEnd(t *testing.T) { if err != nil { panic(err) } - raw, err := ioutil.ReadAll(file) + raw, err := io.ReadAll(file) if err != nil { panic(err) } @@ -56,7 +56,7 @@ func TestEndToEnd(t *testing.T) { } // Generate random test vectors - freshTestVectors, err := generateFreshTestVectors() + freshTestVectors, err := generateFreshTestVectors(20) if err != nil { t.Fatal("Cannot proceed without generated keys: " + err.Error()) } @@ -117,7 +117,7 @@ func decryptionTest(t *testing.T, vector testVector, sk openpgp.EntityList) { t.Fatal(err) } - body, err := ioutil.ReadAll(md.UnverifiedBody) + body, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Fatal(err) } @@ -202,16 +202,22 @@ func encDecTest(t *testing.T, from testVector, testVectors []testVector) { } signKey, _ := signer.SigningKey(time.Now()) expectedKeyID := signKey.PublicKey.KeyId - if md.SignedByKeyId != expectedKeyID { + expectedFingerprint := signKey.PublicKey.Fingerprint + if signKey.PublicKey.Version != 6 && md.SignedByKeyId != expectedKeyID { t.Fatalf( "Message signed by wrong key id, got: %v, want: %v", *md.SignedBy, expectedKeyID) } + if signKey.PublicKey.Version == 6 && !bytes.Equal(md.SignedByFingerprint, expectedFingerprint) { + t.Fatalf( + "Message signed by wrong key id, got: %x, want: %x", + md.SignedByFingerprint, expectedFingerprint) + } if md.SignedBy == nil { t.Fatalf("Failed to find the signing Entity") } - plaintext, err := ioutil.ReadAll(md.UnverifiedBody) + plaintext, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Fatalf("Error reading encrypted contents: %s", err) } diff --git a/openpgp/integration_tests/utils_test.go b/openpgp/integration_tests/utils_test.go index e1ce81fe8..6358af20b 100644 --- a/openpgp/integration_tests/utils_test.go +++ b/openpgp/integration_tests/utils_test.go @@ -17,21 +17,23 @@ import ( // This function produces random test vectors: generates keys according to the // given settings, associates a random message for each key. It returns the // test vectors. -func generateFreshTestVectors() (vectors []testVector, err error) { +func generateFreshTestVectors(num int) (vectors []testVector, err error) { mathrand.Seed(time.Now().UTC().UnixNano()) - for i := 0; i < 3; i++ { + for i := 0; i < num; i++ { config := randConfig() // Sample random email, comment, password and message name, email, comment, password, message := randEntityData() // Only for verbose display v := "v4" - if config.V5Keys { - v = "v5" + if config.V6() { + v = "v6" } pkAlgoNames := map[packet.PublicKeyAlgorithm]string{ - packet.PubKeyAlgoRSA: "rsa_" + v, - packet.PubKeyAlgoEdDSA: "ed25519_" + v, + packet.PubKeyAlgoRSA: "rsa_" + v, + packet.PubKeyAlgoEdDSA: "EdDSA_" + v, + packet.PubKeyAlgoEd25519: "ed25519_" + v, + packet.PubKeyAlgoEd448: "ed448_" + v, } newVector := testVector{ @@ -234,6 +236,8 @@ func randConfig() *packet.Config { pkAlgos := []packet.PublicKeyAlgorithm{ packet.PubKeyAlgoRSA, packet.PubKeyAlgoEdDSA, + packet.PubKeyAlgoEd25519, + packet.PubKeyAlgoEd448, } pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] @@ -261,11 +265,14 @@ func randConfig() *packet.Config { } level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{level} + compConf := &packet.CompressionConfig{Level: level} - var v5 bool + var v6 bool if mathrand.Int()%2 == 0 { - v5 = true + v6 = true + if pkAlgo == packet.PubKeyAlgoEdDSA { + pkAlgo = packet.PubKeyAlgoEd25519 + } } var s2kConf *s2k.Config @@ -282,7 +289,7 @@ func randConfig() *packet.Config { } return &packet.Config{ - V5Keys: v5, + V6Keys: v6, Rand: rand.Reader, DefaultHash: hash, DefaultCipher: ciph, diff --git a/openpgp/integration_tests/v2/end_to_end_test.go b/openpgp/integration_tests/v2/end_to_end_test.go new file mode 100644 index 000000000..d44792375 --- /dev/null +++ b/openpgp/integration_tests/v2/end_to_end_test.go @@ -0,0 +1,400 @@ +// Copyright (C) 2019 ProtonTech AG + +package v2 + +import ( + "bytes" + "crypto" + "encoding/json" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" +) + +///////////////////////////////////////////////////////////////////////////// +// TODO: +// +// - Move signature line endings test to packet unit tests. +// +///////////////////////////////////////////////////////////////////////////// + +type testVector struct { + Message string + Name string + PrivateKey string + PublicKey string + Password string + EncryptedSignedMessage string + config *packet.Config +} + +var allowAllAlgorithmsConfig = packet.Config{ + RejectMessageHashAlgorithms: map[crypto.Hash]bool{}, + RejectPublicKeyAlgorithms: map[packet.PublicKeyAlgorithm]bool{}, + RejectCurves: map[packet.Curve]bool{}, + MinRSABits: 512, +} + +// Takes a set of different keys (some external, some generated here) and test +// interactions between them: encrypt, sign, decrypt, verify random messages. +func TestEndToEnd(t *testing.T) { + // Fetch foreign test vectors from JSON file + file, err := os.Open("testdata/test_vectors.json") + if err != nil { + panic(err) + } + raw, err := io.ReadAll(file) + if err != nil { + panic(err) + } + var foreignTestVectors []testVector + err = json.Unmarshal(raw, &foreignTestVectors) + if err != nil { + panic(err) + } + + for i := 0; i < len(foreignTestVectors); i++ { + foreignTestVectors[i].Name += "_foreign" + config := allowAllAlgorithmsConfig + foreignTestVectors[i].config = &config + } + + // Generate random test vectors + freshTestVectors, err := generateFreshTestVectors(20) + if err != nil { + t.Fatal("Cannot proceed without generated keys: " + err.Error()) + } + testVectors := append(foreignTestVectors, freshTestVectors...) + + // For each testVector in testVectors, (1) Decrypt an already existing message, + // (2) Sign and verify random messages, and (3) Encrypt random messages for + // each of the other keys and then decrypt on the other end. + for _, from := range testVectors { + skFrom := readArmoredSk(t, from.PrivateKey, from.Password) + pkFrom := readArmoredPk(t, from.PublicKey) + t.Run(from.Name, func(t *testing.T) { + + // 1. Decrypt the existing message of the given test vector + t.Run("DecryptPreparedMessage", + func(t *testing.T) { + decryptionTest(t, from, skFrom) + }) + // 2. Sign a message and verify the signature. + t.Run("signVerify", func(t *testing.T) { + t.Run("binary", func(t *testing.T) { + signVerifyTest(t, from, skFrom, pkFrom, true) + }) + t.Run("text", func(t *testing.T) { + signVerifyTest(t, from, skFrom, pkFrom, false) + }) + }) + // 3. Encrypt, decrypt and verify a random message for + // every other key. + t.Run("encryptDecrypt", + func(t *testing.T) { + encDecTest(t, from, testVectors) + }) + }) + } +} + +// This subtest decrypts the existing encrypted and signed message of each +// testVector. +func decryptionTest(t *testing.T, vector testVector, sk openpgp.EntityList) { + if vector.EncryptedSignedMessage == "" { + return + } + prompt := func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + err := keys[0].PrivateKey.Decrypt([]byte(vector.Password)) + if err != nil { + t.Errorf("prompt: error decrypting key: %s", err) + return nil, err + } + return nil, nil + } + sig, err := armor.Decode(strings.NewReader(vector.EncryptedSignedMessage)) + if err != nil { + t.Fatal(err) + } + md, err := openpgp.ReadMessage(sig.Body, sk, prompt, vector.config) + if err != nil { + t.Fatal(err) + } + + body, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatal(err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + + stringBody := string(body) + if stringBody != vector.Message { + t.Fatal("Decrypted body did not match expected body") + } + + // We'll see a sig error here after reading in the UnverifiedBody above, + // if there was one to see. + if err = md.SignatureError; err != nil { + t.Fatal(err) + } + + if md.Signature == nil { + t.Fatal("Expected a signature to be valid") + } +} + +// Given a testVector, encrypts random messages for all given testVectors +// (including self) and verifies on the other end. +func encDecTest(t *testing.T, from testVector, testVectors []testVector) { + skFrom := readArmoredSk(t, from.PrivateKey, from.Password) + // Decrypt private key if necessary + for _, entity := range skFrom { + if err := entity.DecryptPrivateKeys([]byte(from.Password)); err != nil { + t.Error(err) + } + } + pkFrom := readArmoredPk(t, from.PublicKey) + for _, to := range testVectors { + t.Run(to.Name, func(t *testing.T) { + pkTo := readArmoredPk(t, to.PublicKey) + skTo := readArmoredSk(t, to.PrivateKey, to.Password) + message := randMessage() + hints := randFileHints() + + // Encrypt message + signer := skFrom[0] + errDec := signer.PrivateKey.Decrypt([]byte(from.Password)) + if errDec != nil { + t.Error(errDec) + } + buf := new(bytes.Buffer) + w, err := openpgp.Encrypt(buf, pkTo[:1], nil, []*openpgp.Entity{signer}, hints, from.config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) + } + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) + } + + // ----------------- + // On the other end: + // ----------------- + + // Decrypt recipient key + prompt := func(keys []openpgp.Key, symm bool) ([]byte, error) { + err := keys[0].PrivateKey.Decrypt([]byte(to.Password)) + if err != nil { + t.Errorf("Prompt: error decrypting key: %s", err) + return nil, err + } + return nil, nil + } + + // Read message with recipient key + keyring := append(skTo, pkFrom[:]...) + md, err := openpgp.ReadMessage(buf, keyring, prompt, to.config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } + + // Test message details + if !md.IsEncrypted { + t.Fatal("The message should be encrypted") + } + signKey, _ := signer.SigningKey(time.Now(), &allowAllAlgorithmsConfig) + expectedKeyID := signKey.PublicKey.KeyId + expectedFingerprint := signKey.PublicKey.Fingerprint + if len(md.SignatureCandidates) != 1 { + t.Fatal("No signature candidate found") + } + if signKey.PublicKey.Version != 6 && md.SignatureCandidates[0].IssuerKeyId != expectedKeyID { + t.Fatalf( + "Message signed by wrong key id, got: %v, want: %v", + *md.SignatureCandidates[0].SignedBy, expectedKeyID) + } + if signKey.PublicKey.Version == 6 && !bytes.Equal(md.SignatureCandidates[0].IssuerFingerprint, expectedFingerprint) { + t.Fatalf( + "Message signed by wrong key id, got: %x, want: %x", + md.SignatureCandidates[0].IssuerFingerprint, expectedFingerprint) + } + if md.SignatureCandidates[0] == nil { + t.Fatalf("Failed to find the signing Entity") + } + + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + encryptKey, _ := pkTo[0].EncryptionKey(time.Now(), &allowAllAlgorithmsConfig) + expectedEncKeyID := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || + md.EncryptedToKeyIds[0] != expectedEncKeyID { + t.Errorf("Expected message to be encrypted to %v, but got %#v", + expectedKeyID, md.EncryptedToKeyIds) + } + // Test decrypted message + if string(plaintext) != message { + t.Error("decrypted and expected message do not match") + } + + if md.SignatureError != nil { + t.Fatalf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Expected valid signature") + } + }) + } +} + +// Sign a random message and verify signature against the original message, +// another message with same body but different line endings, and a corrupt +// message. +func signVerifyTest( + t *testing.T, + from testVector, + skFrom, pkFrom openpgp.EntityList, + binary bool, +) { + if err := skFrom[0].PrivateKey.Decrypt([]byte(from.Password)); err != nil { + t.Error(err) + } + + messageBody := randMessage() + + // ================================================ + // TODO: Move the line ending checks to unit tests + // ================================================ + // Add line endings to test whether the non-binary version of this + // signature normalizes the final line endings, see RFC4880bis, sec 5.2.1. + lineEnding := " \r\n \n \r\n" + otherLineEnding := " \n \r\n \n" + message := bytes.NewReader([]byte(messageBody + lineEnding)) + otherMessage := bytes.NewReader([]byte(messageBody + otherLineEnding)) + + corruptMessage := bytes.NewReader([]byte(corrupt(messageBody) + lineEnding)) + + // Sign the message + buf := new(bytes.Buffer) + errSign := openpgp.ArmoredDetachSign(buf, skFrom[:1], message, &openpgp.SignParams{ + TextSig: !binary, + Config: &allowAllAlgorithmsConfig, + }) + if errSign != nil { + t.Error(errSign) + } + + // Verify the signature against the corrupt message first + signatureReader := bytes.NewReader(buf.Bytes()) + _, wrongsigner, err := openpgp.VerifyArmoredDetachedSignature( + pkFrom, corruptMessage, signatureReader, &allowAllAlgorithmsConfig) + if err == nil || wrongsigner != nil { + t.Fatal("Expected the signature to not verify") + } + + // Reset the reader and verify against the message with different line + // endings (should pass in the non-binary case) + var errSeek error + _, errSeek = signatureReader.Seek(0, io.SeekStart) + if errSeek != nil { + t.Error(errSeek) + } + + _, otherSigner, err := openpgp.VerifyArmoredDetachedSignature( + pkFrom, otherMessage, signatureReader, &allowAllAlgorithmsConfig) + if binary { + if err == nil || otherSigner != nil { + t.Fatal("Expected the signature to not verify") + return + } + } else { + if err != nil { + t.Fatalf("signature error: %s", err) + } + if otherSigner == nil { + t.Fatalf("signer is nil") + } + if otherSigner.PrimaryKey.KeyId != skFrom[0].PrimaryKey.KeyId { + t.Errorf( + "wrong signer: got %x, expected %x", otherSigner.PrimaryKey.KeyId, 0) + } + } + + // Reset the readers and verify against the exact first message. + _, errSeek = message.Seek(0, io.SeekStart) + if errSeek != nil { + t.Error(errSeek) + } + _, errSeek = signatureReader.Seek(0, io.SeekStart) + if errSeek != nil { + t.Error(errSeek) + } + + _, otherSigner, err = openpgp.VerifyArmoredDetachedSignature( + pkFrom, message, signatureReader, &allowAllAlgorithmsConfig) + + if err != nil { + t.Fatalf("signature error: %s", err) + } + if otherSigner == nil { + t.Fatalf("signer is nil") + } + if otherSigner.PrimaryKey.KeyId != skFrom[0].PrimaryKey.KeyId { + t.Errorf( + "wrong signer: got %x, expected %x", + skFrom[0].PrimaryKey.KeyId, + skFrom[0].PrimaryKey.KeyId, + ) + } +} + +func readArmoredPk(t *testing.T, publicKey string) openpgp.EntityList { + keys, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey)) + if err != nil { + t.Fatal(err) + } + if len(keys) < 1 { + t.Errorf("Failed to read key with good cross signature, %d", len(keys)) + } + if len(keys[0].Subkeys) < 1 { + t.Errorf("Failed to read good subkey, %d", len(keys[0].Subkeys)) + } + return keys +} + +func readArmoredSk(t *testing.T, sk string, pass string) openpgp.EntityList { + keys, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(sk)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Failed to read key with good cross signature, %d", len(keys)) + } + if len(keys[0].Subkeys) < 1 { + t.Errorf("Failed to read good subkey, %d", len(keys[0].Subkeys)) + } + keyObject := keys[0].PrivateKey + if pass != "" { + corruptPassword := corrupt(pass) + if err := keyObject.Decrypt([]byte(corruptPassword)); err == nil { + t.Fatal("Decrypted key with invalid password") + } + } + return keys +} diff --git a/openpgp/integration_tests/v2/testdata/test_vectors.json b/openpgp/integration_tests/v2/testdata/test_vectors.json new file mode 100644 index 000000000..1943e6240 --- /dev/null +++ b/openpgp/integration_tests/v2/testdata/test_vectors.json @@ -0,0 +1,98 @@ +[ + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwYwD4IT3RGwgLJcBA/9txflPrGAhTRBISzQFVrMU2DYjuKy+XbOxMEsNy1H9\neXbCp6lP6AeKxAGrdDfJb209LoL6lvS4UpCV4eV+ucZ1tzZYBlqxTtMq4oC6\nkIYidGJhROe33z3S6ocPN3Q6mYpuu2IT6V1+SBBmYiu3gwMOb8TRQzFcgCOg\nyL3nx4ptEdLAQQEHUZCCsJFFw71i5oZf8/RPd0GM6L0RRG6DH+o/ab8LcT/l\nTXqOfj3KvuHyE5lfwceZUJa0p6zpMLz/Clp/JJnGXtqBPcDlRlKYlot1+LMd\nGYjUOM3S4ObnYOS9of4+6nLzWdl+kK7vHOIfTQPKfc9BuCgkBLwI2FjnJ10B\n4gT495NMTj2IFC2okD5KaGu2rXfNWbS6bJOPWK+zsdwLHUO7PTB9sIDESp+6\noUTn8Fkc5QWKHSbafWIrEWKMLvJBD3HrIbLoGGd1O+RrFuMZV1qsaNh/pqDN\nbBBgRdPauYvDNmUQb9UFfFGiD6GTqNEQd827fz+2r1Lp4OdEdkh1BMeQ\n=wyjK\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "rsa", + "password": "hello world", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\n=lw5e\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+\nfIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5\nGLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0\nJFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS\nYS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6\nAgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki\nBq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf\n9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rC4jQRSYS9OAQQA6R/PtBFa\nJaT4jq10yqASk4sqwVMsc6HcifM5lSdxzExFP74naUMMyEsKHP53QxTF0Grqusag\nQg/ZtgT0CN1HUM152y7ACOdp1giKjpMzOTQClqCoclyvWOFB+L/SwGEIJf7LSCEr\nwoBuJifJc8xAVr0XX0JthoW+uP91eTQ3XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIb\nLgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJSYS9OAAoJEOCE90RsICyXuqIEANmmiRCA\nSF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhP\nGLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2\nbZpsUxRTg9+cSrMWWSNjiY9qUKajm1tuzPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0X\nW748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxqE9+QCEl7qinFqqBLjuzgUhBU4QlwX1GD\nAtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY\nhz3tYjKhoFTKEIq3y3Pp\n=h/aX\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwV4DORo8tfpklnYSAQdAGZTE0CEOk3tK7hRfNjUGPulYp4hL8Y6fScvEj0BdfxAw\nlwQbcr1OhOx4eJKMkY7SK8wFTzkNX4N0FbGcebeZxHm1cP/EI6qwwOxb+XCsuVEK\n0sAUAfVy+S+FAzZpYbq57UUOXAi+be8Q4Q80ZUyFuGHDi0ks8D6b/19WV6tuUBv9\npx9X01IUcUht9rDCYTRhMRDSaUVXYf1JKEeLEiHVbG9GDXOGg167A5ajuiDM3T3G\n6ZSYdt5LSfca5J/RKIhLUvcJzzWeZMPnT0R7s9HhtOAI6eJaGY+YqEg4ilyppc9i\nW2BVaT/cYvApTt7E4ty2feoeHN/ZNBmySasNTLXhANTESEQg0MEU03w7H/yGtC67\nXKgzviBg3KJoO3o/wofmAlmS99jIxAI=\n=WzB1\n-----END PGP MESSAGE--------", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", + "name": "curve25519", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlFgEYvTtQBYJKwYBBAHaRw8BAQdAxsNXLbrk5xOjpO24VhOMvQ0/F+JcyIkckMDH\nX3FIGxcAAQDFOlunZWYuPsCx5JLp78vKqUTfgef9TGG4oD6I/Sa0zBMstCJHb2xh\nbmcgR29waGVyIDxnb2xhbmdAZXhhbXBsZS5vcmc+iJAEExYIADgWIQSFQHEOazmo\nh1ldII4MvfnLQ4JBNwUCYvTtQAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK\nCRAMvfnLQ4JBN5yeAQCKdry8B5ScCPrev2+UByMCss7Sdu5RhomCFsHdNPLcKAEA\n8ugei+1owHsV+3cGwWWzKk6sLa8ZN87i3SKuOGp9DQycXQRi9O1AEgorBgEEAZdV\nAQUBAQdA5CubPp8l7lrVQ25h7Hx5XN2C8xanRnnpcjzEooCaEA0DAQgHAAD/Rpc+\nsOZUXrFk9HOWB1XU41LoWbDBoG8sP8RWAVYwD5AQRYh4BBgWCAAgFiEEhUBxDms5\nqIdZXSCODL35y0OCQTcFAmL07UACGwwACgkQDL35y0OCQTcvdwEA7lb5g/YisrEf\niq660uwMGoepLUfvtqKzuQ6heYe83y0BAN65Ffg5HYOJzUEi0kZQRf7OhdtuL2kJ\nSRXn8DmCTfEB\n=cELM\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEYvTtQBYJKwYBBAHaRw8BAQdAxsNXLbrk5xOjpO24VhOMvQ0/F+JcyIkckMDH\nX3FIGxfNIkdvbGFuZyBHb3BoZXIgPGdvbGFuZ0BleGFtcGxlLm9yZz7CkAQTFggA\nOBYhBIVAcQ5rOaiHWV0gjgy9+ctDgkE3BQJi9O1AAhsDBQsJCAcCBhUKCQgLAgQW\nAgMBAh4BAheAAAoJEAy9+ctDgkE3nJ4BAIp2vLwHlJwI+t6/b5QHIwKyztJ27lGG\niYIWwd008twoAQDy6B6L7WjAexX7dwbBZbMqTqwtrxk3zuLdIq44an0NDM44BGL0\n7UASCisGAQQBl1UBBQEBB0DkK5s+nyXuWtVDbmHsfHlc3YLzFqdGeelyPMSigJoQ\nDQMBCAfCeAQYFggAIBYhBIVAcQ5rOaiHWV0gjgy9+ctDgkE3BQJi9O1AAhsMAAoJ\nEAy9+ctDgkE3L3cBAO5W+YP2IrKxH4quutLsDBqHqS1H77ais7kOoXmHvN8tAQDe\nuRX4OR2Dic1BItJGUEX+zoXbbi9pCUkV5/A5gk3xAQ==\n=OBcx\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwXYDUzQX5WbZ2DUSAcdAL8N/KsIV0/O1DJTQ9ZX2gSet+z4fpHlNGS0md8GNF8++\n7pt+XXGykKLnXDxtbMOQJGRYGbL0svgwpoHwU8eRXgDdcMr4GQwGlAz1GGQqfVyu\nkLwA3PRXR2zMtVQ3EU1+H4zrKcizA49j0sBHAQbGxg1ZU+6nbGXBLYdLs6y4+WcO\nozq45Jucl9cABn8uX9P4hXnncCAnMpMn4DkCPdQ9L+sTLER3WmqLvu/jQ7gMiqgs\nPkvgmt/qtl1NhS6ghnqrRSg3rxAZC9cA2hWV2cmLX9/h7MeiCn/78FIfH5XWOxKW\nMN9zYgzaMZ3M12hbw3/ma/qtXeHc1TeJ7fYGfwDBHIEFUPHtj685XfztcYsPoBi7\nQK4E+RFrpfyzhoU5HYJGsuuQXZIXAQlN6qCM1s9QwVk3zDw4d+H9JJI7YN3FoBrV\n5O9T2aU8Vnnpk6t+sDDZuoW8Igd/OYJkZGspm9ExY0n1mOVEW28NecVtW2nbmcDn\nwLg=\n=A1LE\n-----END PGP MESSAGE--------", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", + "name": "curve448", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\nQgqsfguR1PqPuJxpXV4bSr6CGAAAAA==\n=MSvh\n-----END PGP PRIVATE KEY BLOCK-----\n", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxkYEYV2UmRYDK2VxAc9AFyxgh5xnSbyt50TWl558mw9xdMN+/UBLr5+UMP8IsrvV\nMdXuTIE8CyaUQKSotHtH2RkYEXj5nsMAzRFDdXJ2ZTQ0OCBPcHRpb24gOMLARwQT\nFgoAhgWCYV2UmQWJBaSPvQMLCQcJEKaZmJX6us5SRxQAAAAAAB4AIHNhbHRAbm90\nYXRpb25zLnNlcXVvaWEtcGdwLm9yZw+eVLGDMxOjgAj/poMOc70/gQYKJfP7hUIx\n4OJK41AjAxUKCAKZAQKbAQIeARYhBMHbZdWA17kiclRLHqaZmJX6us5SAAD0CQOX\nQNF6sM5/9LsAqkZbDlQgd3+TTo9XsSp5Qb0cNMU9VQXntcsb5VLeXiAKAsVqEZVg\nSaG9005IZvQBAPHcocD5shn1h8z8OTRRt8bF62XTVzb4rfVjFQD3JrMke7Xl7g5S\nSLcUQHSSXP6nyWFWwRaGja4WAAAAzkYEYV2UmRYDK2VxAc9AS+0x2fTJwi30jydR\nTC4pvXhpENwX5zN4XuazsDt7deECNS4yPejo5HiYqTIHBQJhD9Qav2Rxju6AwsEs\nBBgWCgFrBYJhXZSZBYkFpI+9CRCmmZiV+rrOUkcUAAAAAAAeACBzYWx0QG5vdGF0\naW9ucy5zZXF1b2lhLXBncC5vcmcURFIs3U3HqQc4EjDr9lVkAlm5fvtwmRG4AFg6\n8r0rMgKbAsAxoAQZFgoAbwWCYV2UmQkQSqN0i+EuXY5HFAAAAAAAHgAgc2FsdEBu\nb3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnpqBkw45jUIiM8OhzhHXb0tDge1Gfz9O4\nzPxFZp2MU5QWIQQMbJ5BqcdVAoh+oFFKo3SL4S5djgAAC2EDl0ADAdaCqjlsUhZr\n5oAijXGwAMgN6YheRESKrOM9O6Cb6g2Kzlre+VLBCB/9OIFP7x3OBJnxtiQyyYDi\n5ck1D5C/9PH9MnRmT6HxUAYZ/B5LhRY5zbSeaM9xiD+HHVjEBWlMNLr42zzPU5t6\nbRDRnek7NQAAABYhBMHbZdWA17kiclRLHqaZmJX6us5SAABqUwOXQE0eolNhq6VH\nEIKRvianwLBpheMgDoYBcXNVH4nRZN507qd3y4I4qZGGNe9Fi0TW8ipFTkqHcfdx\ngLOfK4sztWnAwFoi6t3MhDwRV28hU4W3DT3C5fv++1JP5oJGKz0BQEFnStZEkZuE\noswwSt+yvAoQAAAAzkkEYV2UmRIDK2VvAcdAzIc74UKPjrF3qkOkFcOLBQmnsR4e\nqy6IYe08y94UEo6Cdu31eiQZCFnZ9C36zJSSA20r9ki3ZYEDAQoJwsA5BBgWCgB4\nBYJhXZSZBYkFpI+9CRCmmZiV+rrOUkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\nZXF1b2lhLXBncC5vcmcSMExACZz/a7jm7kVAaYFADK2NIbYszclA3489WXm8SQKb\nDBYhBMHbZdWA17kiclRLHqaZmJX6us5SAABJ1gOXQNcQdD3KQ0/atk/fkRPV2aIV\nqAW1OP+EtBxaWDXouq8k0ao6Bhe6pN6tuI463TZcsIGWf7B17ClTgKER98xQNEF+\np9Byj9iwypZXtsckcA7R4L8MwoKPJT3TF0ftH91CCqx+C5HU+o+4nGldXhtKvoIY\nAAAA\n=qcl0\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwcBOA1N4OCSSjECBEAP8DhX4Ii5TxauisNiJ6ThzZVo0rDrM37eG55Z9/Fp9\nwOFcMoYiM7muadPd0jjVkGk0Y0d1QrfmAW1619L3kv4lGJcB92jEVXeg6HPq\nyLVEc2KzvyIO2ypZ6CBlYhz1iWtc29tgbf1BkVjNGk8C1OIauCqQtNHDwpso\ntFF29gfHKbwEAIkeoyCs85tAyJnNWMrEyMo+GSico4uVEiJCw4DD65O4pW3Y\ns0PUj9HhE8CY01zKADsn9CHo2P0eppbw/7H++ViHdFzkcbrz6Tqt43tC9B29\nNBPdnhMlyJJhivW1FvLoPpuLiYpNb9Dv2lTpug5UUVZR6q9HTuvhP7PJuo5J\n3MIh0qsByqXXlrAZuvZtIZVYX9hFLK7AlLQ4BIbJ5ZoTDOMlamviiKEs/Txj\npBbKbBAQW+fw6ajsKSNoWPqYriVEOGtKCfmrCTe32W0Diifyap7VbsY5q9yK\n07XbMTDZgtxByDMJ9YLdjG2+J9jkQyKoh8SioWZCeRwsUJOjMTVdfbDAeNId\n7me65b7rhtbiR3lU60l5CANdQi+cHTyh3azeFqUqZ5UFNEY8mUXzVWw=\n=+rSf\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "dsa", + "password": "abcd", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQHhBFERnrMRBADmM0hIfkI3yosjgbWo9v0Lnr3CCE+8KsMszgVS+hBu0XfGraKm\nivcA2aaJimHqVYOP7gEnwFAxHBBpeTJcu5wzCFyJwEYqVeS3nnaIhBPplSF14Duf\ni6bB9RV7KxVAg6aunmM2tAutqC+a0y2rDaf7jkJoZ9gWJe2zI+vraD6fiwCgxvHo\n3IgULB9RqIqpLoMgXfcjC+cD/1jeJlKRm+n71ryYwT/ECKsspFz7S36z6q3XyS8Q\nQfrsUz2p1fbFicvJwIOJ8B20J/N2/nit4P0gBUTUxv3QEa7XCM/56/xrGkyBzscW\nAzBoy/AK9K7GN6z13RozuAS60F1xO7MQc6Yi2VU3eASDQEKiyL/Ubf/s/rkZ+sGj\nyJizBACtwCbQzA+z9XBZNUat5NPgcZz5Qeh1nwF9Nxnr6pyBv7tkrLh/3gxRGHqG\n063dMbUk8pmUcJzBUyRsNiIPDoEUsLjY5zmZZmp/waAhpREsnK29WLCbqLdpUors\nc1JJBsObkA1IM8TZY8YUmvsMEvBLCCanuKpclZZXqeRAeOHJ0v4DAwK8WfuTe5B+\nM2BOOeZbN8BpfiA1l//fMMHLRS3UvbLBv4P1+4SyvhyYTR7M76Q0xPc03MFOWHL+\nS9VumbQWVGVzdDIgPHRlc3QyQHRlc3QuY29tPohiBBMRAgAiBQJREZ6zAhsDBgsJ\nCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRARJ5QDyxae+MXNAKCzWSDR3tMrTrDb\nTAri73N1Xb3j1ACfSl9y+SAah2q7GvmiR1+6+/ekqJGdAVgEURGesxAEANlpMZjW\n33jMxlKHDdyRFXtKOq8RreXhq00plorHbgz9zFEWm4VF53+E/KGnmHGyY5Cy8TKy\nZjaueZZ9XuG0huZg5If68irFfNZtxdA26jv8//PdZ0Uj+X6J3RVa2peMLDDswTYL\nOL1ZO1fxdtDD40fdAiIZ1QyjwEG0APtz41EfAAMFBAC5/dtgBBPtHe8UjDBaUe4n\nNzHuUBBp6XE+H7eqHNFCuZAJ7yqJLGVHNIaQR419cNy08/OO/+YUQ7rg78LxjFiv\nCH7IzhfU+6yvELSbgRMicY6EnAP2GT+b1+MtFNa3lBGtBHcJla52c2rTAHthYZWk\nfT5R5DnJuQ2cJHBMS9HWyP4DAwK8WfuTe5B+M2C7a/YJSUv6SexdGCaiaTcAm6g/\nPvA6hw/FLzIEP67QcQSSTmhftQIwnddt4S4MyJJH3U4fJaFfYQ1zCniYJohJBBgR\nAgAJBQJREZ6zAhsMAAoJEBEnlAPLFp74QbMAn3V4857xwnO9/+vzIVnL93W3k0/8\nAKC8omYPPomN1E/UJFfXdLDIMi5LoA==\n=LSrW\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxsDiBFERnrMRBADmM0hIfkI3yosjgbWo9v0Lnr3CCE+8KsMszgVS+hBu0XfG\nraKmivcA2aaJimHqVYOP7gEnwFAxHBBpeTJcu5wzCFyJwEYqVeS3nnaIhBPp\nlSF14Dufi6bB9RV7KxVAg6aunmM2tAutqC+a0y2rDaf7jkJoZ9gWJe2zI+vr\naD6fiwCgxvHo3IgULB9RqIqpLoMgXfcjC+cD/1jeJlKRm+n71ryYwT/ECKss\npFz7S36z6q3XyS8QQfrsUz2p1fbFicvJwIOJ8B20J/N2/nit4P0gBUTUxv3Q\nEa7XCM/56/xrGkyBzscWAzBoy/AK9K7GN6z13RozuAS60F1xO7MQc6Yi2VU3\neASDQEKiyL/Ubf/s/rkZ+sGjyJizBACtwCbQzA+z9XBZNUat5NPgcZz5Qeh1\nnwF9Nxnr6pyBv7tkrLh/3gxRGHqG063dMbUk8pmUcJzBUyRsNiIPDoEUsLjY\n5zmZZmp/waAhpREsnK29WLCbqLdpUorsc1JJBsObkA1IM8TZY8YUmvsMEvBL\nCCanuKpclZZXqeRAeOHJ0s0WVGVzdDIgPHRlc3QyQHRlc3QuY29tPsJiBBMR\nAgAiBQJREZ6zAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRARJ5QD\nyxae+MXNAKCzWSDR3tMrTrDbTAri73N1Xb3j1ACfSl9y+SAah2q7GvmiR1+6\n+/ekqJHOwE0EURGesxAEANlpMZjW33jMxlKHDdyRFXtKOq8RreXhq00plorH\nbgz9zFEWm4VF53+E/KGnmHGyY5Cy8TKyZjaueZZ9XuG0huZg5If68irFfNZt\nxdA26jv8//PdZ0Uj+X6J3RVa2peMLDDswTYLOL1ZO1fxdtDD40fdAiIZ1Qyj\nwEG0APtz41EfAAMFBAC5/dtgBBPtHe8UjDBaUe4nNzHuUBBp6XE+H7eqHNFC\nuZAJ7yqJLGVHNIaQR419cNy08/OO/+YUQ7rg78LxjFivCH7IzhfU+6yvELSb\ngRMicY6EnAP2GT+b1+MtFNa3lBGtBHcJla52c2rTAHthYZWkfT5R5DnJuQ2c\nJHBMS9HWyMJJBBgRAgAJBQJREZ6zAhsMAAoJEBEnlAPLFp74QbMAn3V4857x\nwnO9/+vzIVnL93W3k0/8AKC8omYPPomN1E/UJFfXdLDIMi5LoA==\n=Oa9H\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwX4DaAJTyVNTRvUSAgMERFsv0Org2FHPu0n5k6xNOv520Yh2dk2SDojc6cF3\nynPgyMftshAfmDQZ6zPDwW1Ya8EB9ihsXcbjBg4Uf1xoBjCdQHkVTjI39ehZ\nfLltFzdpW+HDNL7zx9TjqBi/dWwOIJCSccIb1iOTiCTXTh9X35LSwAMBJtMw\nmTOOAKH9hKXOTYIH3EWasWWzSOOqdndYppsiPYBFAUzq9IK07FNU2wb4/Pat\nqILTqW783FiECucSCw1gkpWEJVxmh9+gys0JrHLruECY88jK1ujcZL7079gU\n546Txtrs8qRso4zCoD/MWwawgdIzdHuRwTo/rwt7jMjMfinZ8/t/CIxyCqva\nwLtajClaU5zuD3uGSagSQ6ZZ2ga5Q9XStzZfsuS3yW6qfD/b3Vu3SuRoZGjV\nYUjQR2GU+zI4qrw=\n=GXt6\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "p256", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlHcEWqvIexMIKoZIzj0DAQcCAwTHEN/Yb0iLnIdL1TZcPDB2k+KqSnMlOxiK2YwV\nxd9or0tNccGkt7Sg3NcNua7X/YW45Vgkxq0p9lf3pJsepydVAAD/SqMfMs2IAGx3\nm0Sv1Jr43iXBQUZiSW+YXfIvqFNm+s8RDrQfdGVzdCB0ZXN0IDxtY3Rlc3R5QGV4\nYW1wbGUub3JnPoiQBBMTCAA4FiEECufLSTsSypWIu9QDLSA1IA76sfYFAlqryHsC\nGwMFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQLSA1IA76sfYnfgD+PPwhSbEA\nJaE1Vh4UIea7TYAz3XnZp9F8C6B9NWPQWT0A/R0630k5W9uSmdlqP16+LXzs4rdM\nbGxSVSBcXVyx9eIWnHsEWqvIexIIKoZIzj0DAQcCAwTwYoek6LUhUSC1ApqXj6xk\nCYRH8H+sFLWAcf+P+zMvaR6v49qJl6Y1VYtEgwnpNnQhcx7Vrlul0FkisFqcwfXV\nAwEIBwAA/j5HDZp4rZI4KIni08VGpl70oRWdud8zrP8lQpsqZ6n8EYOIeAQYEwgA\nIBYhBArny0k7EsqViLvUAy0gNSAO+rH2BQJaq8h7AhsMAAoJEC0gNSAO+rH2hhkA\n/14tdEa6SuCtFC0vE+c8pAcs2YNiu3cXDFEyg/3PAUbQAQCOfvI2+mawOH6GJzsz\nvhrof7LYUWcOUggO69XoCM9Log==\n=bVnA\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxlIEWqvIexMIKoZIzj0DAQcCAwTHEN/Yb0iLnIdL1TZcPDB2k+KqSnMlOxiK\n2YwVxd9or0tNccGkt7Sg3NcNua7X/YW45Vgkxq0p9lf3pJsepydVzR90ZXN0\nIHRlc3QgPG1jdGVzdHlAZXhhbXBsZS5vcmc+wpAEExMIADgWIQQK58tJOxLK\nlYi71AMtIDUgDvqx9gUCWqvIewIbAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIX\ngAAKCRAtIDUgDvqx9id+AP48/CFJsQAloTVWHhQh5rtNgDPdedmn0XwLoH01\nY9BZPQD9HTrfSTlb25KZ2Wo/Xr4tfOzit0xsbFJVIFxdXLH14hbOVgRaq8h7\nEggqhkjOPQMBBwIDBPBih6TotSFRILUCmpePrGQJhEfwf6wUtYBx/4/7My9p\nHq/j2omXpjVVi0SDCek2dCFzHtWuW6XQWSKwWpzB9dUDAQgHwngEGBMIACAW\nIQQK58tJOxLKlYi71AMtIDUgDvqx9gUCWqvIewIbDAAKCRAtIDUgDvqx9oYZ\nAP9eLXRGukrgrRQtLxPnPKQHLNmDYrt3FwxRMoP9zwFG0AEAjn7yNvpmsDh+\nhic7M74a6H+y2FFnDlIIDuvV6AjPS6I=\n=UrtO\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwZ4D3tsm495R/DQSAwME/mB7VDjlMWyJzp0BHit9A4M5hFCHSSI70xNeXAqP\n+eziDTAoo/J3ulVEVx6dyBvXizBxHIz4F5y8eQPfiz8zgj7572z5kuH+/OIh\n+AZzEdIxnH/WkQs9sT9N9ostKXUMMOimh1DXPfEExQ4IFTZDnVEA3fCpFmFO\nPzNAArFWk8lM1AaEbu5Z3EPTWQRbY6YSrdLAIwFa2gjuQViUs4akHiCXNq6C\nN31ebwBwR/Ax6BKE8H8ruYzNFviz5o6Uw0oDCbaSR9hRTptnA9Hfvr+Dp9XT\n/ojG+YYq/gOQSycYB2MrBiIFRkuxwB86Ngq/WNSzMxtsjod4BlCtqmSys9Sj\n/D9VySgTdvfBR6ukYMrC0lOgZBkJNtQNuY5qFePTTWQ4s9CdvSKoQQRbUY2V\nNWIGuDOafFICpaD5/VQakAemYOCBKks3F+zbHW6M7g/dYt9zZECOCBwrH+1m\nho/jQzANS17yy2O17IXqZUvroDfNjdY8Rw/fiCTL\n=cfGW\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "p384", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlKQEWqvMThMFK4EEACIDAwQeVELezSIjmPkpfo3QejOWQwPxxaA6xnh3Lgu0zoTz\njbYeE6xFejlLMuHGRs/msuwkqRIEKPufVxDA9t4llIClJus82Bei2FV6gF+21xdI\nMynwilwV6hwya6TWWC6sqOMAAX9cMXKSG1fl2yoKM9G95d6+YENwzp5EdOtCKrtL\nBOPuEu4yXVNQAxlep0+/MFirWwYWF7QeQmlnIHRlc3QgPEJpZ1Rlc3RAZXhhbXBs\nZS5vcmc+iLAEExMJADgWIQRz2hPLfQfnxiN4c7b9XzwLxz7JSgUCWqvMTgIbAwUL\nCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRD9XzwLxz7JSnyOAYCc0Qm/4TukrxOz\n9quCjxjXUC0FJBePOuXjMFHpbRC8/w6Sm/MmL2wv4ogfUlseh2EBgJvUcPmuLG5P\nY/sbpPFa6lkWe9mqWNZA55u8FrO8NhmoT4vpGT/L79PeFZayXR+T0pyoBFqrzE4S\nBSuBBAAiAwMEB/yKSJ1l7arWVl3fCt1NmQpGHl3kahzKcymyA0aBisLs9igedPyp\nEEnbahUpiwfrQR41hLeL07co/3dceUkcCyuuaJSnR1PrGx/WATvfHVjjFFXJK0L3\nlhHwWUISBre+AwEJCQABgN/tAcIomJ3bqh76v8X962P1h/w0W4K9K8F+cypjCm4S\n8a0Xfs7oMRTd/FDLU6OB3huKiJgEGBMJACAWIQRz2hPLfQfnxiN4c7b9XzwLxz7J\nSgUCWqvMTgIbDAAKCRD9XzwLxz7JSnwTAX9bU7QVcIKnNq15eydUBDsblk0exu0C\nepKHjB4WAp8UDKREvG4jhMxlvEa12vWb3yMBgMCjAgQ7WdzJanTZLi6bNyGO3ptg\ng++gKRKmxI7Jg0+oAOcL4v2iuUx6Yo66T67gCg==\n=CW/l\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxm8EWqvMThMFK4EEACIDAwQeVELezSIjmPkpfo3QejOWQwPxxaA6xnh3Lgu0\nzoTzjbYeE6xFejlLMuHGRs/msuwkqRIEKPufVxDA9t4llIClJus82Bei2FV6\ngF+21xdIMynwilwV6hwya6TWWC6sqOPNHkJpZyB0ZXN0IDxCaWdUZXN0QGV4\nYW1wbGUub3JnPsKwBBMTCQA4FiEEc9oTy30H58YjeHO2/V88C8c+yUoFAlqr\nzE4CGwMFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQ/V88C8c+yUp8jgGA\nnNEJv+E7pK8Ts/argo8Y11AtBSQXjzrl4zBR6W0QvP8OkpvzJi9sL+KIH1Jb\nHodhAYCb1HD5rixuT2P7G6TxWupZFnvZqljWQOebvBazvDYZqE+L6Rk/y+/T\n3hWWsl0fk9LOcwRaq8xOEgUrgQQAIgMDBAf8ikidZe2q1lZd3wrdTZkKRh5d\n5GocynMpsgNGgYrC7PYoHnT8qRBJ22oVKYsH60EeNYS3i9O3KP93XHlJHAsr\nrmiUp0dT6xsf1gE73x1Y4xRVyStC95YR8FlCEga3vgMBCQnCmAQYEwkAIBYh\nBHPaE8t9B+fGI3hztv1fPAvHPslKBQJaq8xOAhsMAAoJEP1fPAvHPslKfBMB\nf1tTtBVwgqc2rXl7J1QEOxuWTR7G7QJ6koeMHhYCnxQMpES8biOEzGW8RrXa\n9ZvfIwGAwKMCBDtZ3MlqdNkuLps3IY7em2CD76ApEqbEjsmDT6gA5wvi/aK5\nTHpijrpPruAK\n=DewR\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwcACAzZYNqaBUBrOEgQjBABmhjNT+HfNdK3mVvVRpIbP8BACPUzmnFagNzd7\nd4jFqfRrP3Il3ohx+scNEYxFgloGOooukRJXASauk4MUXgvpFAFtycVNTT3N\nqgeBQi+j1BLBV53KG+nyxEQRGR9QaDgYjjH0OvyhJbe7Ov9ESwN1nMUH4/Xq\n+Zm/vy3/ysRCZ/AYMDAYB238b3OAPjLsAJ+VR6YgJfYK1R4B6+bKM/LufZ7U\nN2w6dZtOANXgTfd9JMxIscXSwEUB3YZCrnOsII6ryhkZybjK00n0PRQS4jgK\nJ8zdv5MvTdYfD3sVPjp7dnZwsuwkipnZEt0l/nMtVvI7l6XnN04RNEK4Elor\nD5qRKRI0XcFLaJaZWDh51Lp0kE0iGTkzDI0zhnzh1TY0UyqpTfQ9hvvGWY+s\n71oUqUMv3Izq13LUGDFAru9WXxDddATmSZR4PAmUZnHL5cDBK+pLHLjE31c9\nIYFwLjDNuBVJ+1BAtP9kvi70j0Nvv7yFfWeeXCN2IFsWe2Mk9108GX5Ls+ta\nnmZtITdvQ334bCuvYkxUhdZxZZ4z134uHdqbC0BPtE9ohRUsuhXi+dxXE3Iz\nUC6h8O9obgz9IN4=\n=/S1e\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "p521", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlNoEWqvMtxMFK4EEACMEIwQBG+c6zB/CLvG89VPlwcEf7lVw1o/USkR1BKwdEJX+\n6kevyCfoW9i5fr3Xj3te/KEkWbm53fYCxQxT1YPCOEQe2/gAyH+Sa+xIL3WFxOho\nHLumFLsjhTxI1HO24UNYwrtanYUXJUyZdBdbE3iC0S0pDEVYvCNjNU85tqS5lulh\nMc6xIp0AAgkBlHig6LGOdFTLzkeKJZPTvb//E6lBXuvfbA4plFlg907OM7c6PEUv\nnoRvR4cNkKDdShJxBPVlZVkbtiMT7H3JmcEgCLQbdGVzdGVyIEEgPHRlc3RAZXhh\nbXBsZS5vcmc+iNIEExMKADgWIQSlBvevyCa+SH3t6owrJYFtVTYvDgUCWqvMtwIb\nAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRArJYFtVTYvDn1FAgYuArHX6gpb\nFPIo97qMNU+OumyOgzn93w7RzdJUF1Pvyjfib8qAoFc2RbD2B4O10UcFGR/tvY7I\nswYU25vwrdpkwQIIy2gvuosOwiHwWOGq7vIPhZX2XiUUCg/IhbbiQVYHMJXgWFuq\nm5xbfo5yjBk7fAxyzg2JUbwr5odAGKkXM2zeVFuc3QRaq8y3EgUrgQQAIwQjBAFw\n35IH/Ksef/VTf0fuv831HngtCqcW3EBKZT2+eiQcfk6I0mY7fNIHRT2tsc9QBTNk\nD5smT8CX9ZnolCOetL9EywBJZp1c3yns6Nk091/K1d9kCLkckRbIm0yS6+/qA0cM\nFZZbRfIGHXgyKkG2IZ3diWQIGkYiwx8VtZzmVD/Nx5PhdgMBCgkAAgdbkreSrYh8\n5kgEQQsJyLOJVPyS11qqyegmBOOXu4+KDXaKNXeLRszk7mMCdUxLqUeWMrivy0vl\nSerrL+aazVYaDCEqiLoEGBMKACAWIQSlBvevyCa+SH3t6owrJYFtVTYvDgUCWqvM\ntwIbDAAKCRArJYFtVTYvDq2GAgj3HDhyoxdsFkWJ986M7zTijTDHR3D4SVoRu5IB\nAsIGDpKBYdYzOEweShT0usxORdha1pIX1M1h8nk6CMm3WHhcxAIHWV9aOdGIhvcT\nFBv4c44lu5xpvsgGgmDaIwlgLunElVMnKSXrO9Hqpn9a+pRJv5Be/BsZOW82Y2f7\n/K7TUzYNJa4=\n=dTWU\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxpMEWqvMtxMFK4EEACMEIwQBG+c6zB/CLvG89VPlwcEf7lVw1o/USkR1BKwd\nEJX+6kevyCfoW9i5fr3Xj3te/KEkWbm53fYCxQxT1YPCOEQe2/gAyH+Sa+xI\nL3WFxOhoHLumFLsjhTxI1HO24UNYwrtanYUXJUyZdBdbE3iC0S0pDEVYvCNj\nNU85tqS5lulhMc6xIp3NG3Rlc3RlciBBIDx0ZXN0QGV4YW1wbGUub3JnPsLA\nEgQTEwoAOBYhBKUG96/IJr5Ife3qjCslgW1VNi8OBQJaq8y3AhsDBQsJCAcC\nBhUICQoLAgQWAgMBAh4BAheAAAoJECslgW1VNi8OfUUCBi4CsdfqClsU8ij3\nuow1T466bI6DOf3fDtHN0lQXU+/KN+JvyoCgVzZFsPYHg7XRRwUZH+29jsiz\nBhTbm/Ct2mTBAgjLaC+6iw7CIfBY4aru8g+FlfZeJRQKD8iFtuJBVgcwleBY\nW6qbnFt+jnKMGTt8DHLODYlRvCvmh0AYqRczbN5UW86XBFqrzLcSBSuBBAAj\nBCMEAXDfkgf8qx5/9VN/R+6/zfUeeC0KpxbcQEplPb56JBx+TojSZjt80gdF\nPa2xz1AFM2QPmyZPwJf1meiUI560v0TLAElmnVzfKezo2TT3X8rV32QIuRyR\nFsibTJLr7+oDRwwVlltF8gYdeDIqQbYhnd2JZAgaRiLDHxW1nOZUP83Hk+F2\nAwEKCcK6BBgTCgAgFiEEpQb3r8gmvkh97eqMKyWBbVU2Lw4FAlqrzLcCGwwA\nCgkQKyWBbVU2Lw6thgII9xw4cqMXbBZFiffOjO804o0wx0dw+ElaEbuSAQLC\nBg6SgWHWMzhMHkoU9LrMTkXYWtaSF9TNYfJ5OgjJt1h4XMQCB1lfWjnRiIb3\nExQb+HOOJbucab7IBoJg2iMJYC7pxJVTJykl6zvR6qZ/WvqUSb+QXvwbGTlv\nNmNn+/yu01M2DSWu\n=KibM\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwX4DDYFqRW5CSpsSAgMEre9Mf7Ig5et7Z+E6dTM/pTEKD8cEIfuW5yV8RL2X\n3FqGkGbhpmxgyIrWvf3cJhhmusdkzl+AisnGz71bVgfBYjCfe3olAfyvlZUj\n0Bf0pPh9EfdD5dFLf5XXXkXdOwuUeW6UffdrYA14avHkjqmoE9fSwAMBc4Pm\nEiOLe7iNZgYtTzvCXei213SCdN17sixw5c/iroY1QuFwECMdlQG0X9qG4Ddp\nJCnW+dPZ9lHCD+NyaWoY3QAAMyMTlMeysj19o7LHG4+fgUZL2XBGcrnZkgmF\ni4YsT9fLuVf/cPPnBI+grrQPNEzZim1FgBjcgD7QgoPbrATtq63lEyJbjwAw\nKC38/emOZdwHe3RyhJSFdJ0B6VVz9nJuYvncE1VkuZVxaRhtE2FmZutBC/cy\nUMcIpI0EsrGSCI8=\n=8BUx\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "secp256k1", + "password": "juliet", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nxaIEVjET2xMFK4EEAAoCAwS/zT2gefLhEnISXN3rvdV3eD6MVrPwxNMAR+LM\nZzFO1gdtZbf7XQSZP02CYQe3YFrNQYYuJ4CGkTvOVJSV+yrA/gkDCILD3FP2\nD6eRYNWhI+QTFWAGDw+pIhtXQ/p0zZgK6HSk68Fox0tH6TlGtPmtULkPExs0\ncnIdAVSMHI+SnZ9lIeAykAcFoqJYIO5p870XbjzNLlJvbWVvIE1vbnRhZ3Vl\nIChzZWNwMjU2azEpIDxyb21lb0BleGFtcGxlLm5ldD7CcgQQEwgAJAUCVjET\n2wULCQgHAwkQwrEjibQBpD0DFQgKAxYCAQIbAwIeAQAA6McBAMzLoQk8VrIK\nAjhlKZmVK57fEVKGP3LqFlMtmo8mq6ylAP4wxPTRrYKJyduGt3XxJBQhhFD/\n8juhdWjFk3kwDiuY4cemBFYxE9sSBSuBBAAKAgMEs9mAYLbntFpn0GDeKP06\nBKgkbCoi7TdJ9AxOdHZyseQAeotpQibP+XCGwv9Xj33NdnaASZ+goJpyuhc0\nMzznFQMBCAf+CQMIqp5StLTK+lBgqmaJ8/64E+8+OJVOgzk8EoRp8bS9IEac\nVYu2i8ARjAF3sqwGZ5hxxsniORcjQUghf+n+NwEm9LUWfbAGUlT4YfSIq5pV\nrsJhBBgTCAATBQJWMRPbCRDCsSOJtAGkPQIbDAAA5IMBAOAd5crBXv9/ihPz\n4AkmZFH8e5+8ZSqjIqq1/lTcj3o/AQDECYLTFXbJUBy6+X4aBp0aLbNyUgtI\n9tFEEDilTl2ltw==\n=C3TW\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EVjET2xMFK4EEAAoCAwS/zT2gefLhEnISXN3rvdV3eD6MVrPwxNMAR+LM\nZzFO1gdtZbf7XQSZP02CYQe3YFrNQYYuJ4CGkTvOVJSV+yrAzS5Sb21lbyBN\nb250YWd1ZSAoc2VjcDI1NmsxKSA8cm9tZW9AZXhhbXBsZS5uZXQ+wnIEEBMI\nACQFAlYxE9sFCwkIBwMJEMKxI4m0AaQ9AxUICgMWAgECGwMCHgEAAOjHAQDM\ny6EJPFayCgI4ZSmZlSue3xFShj9y6hZTLZqPJquspQD+MMT00a2Cicnbhrd1\n8SQUIYRQ//I7oXVoxZN5MA4rmOHOUwRWMRPbEgUrgQQACgIDBLPZgGC257Ra\nZ9Bg3ij9OgSoJGwqIu03SfQMTnR2crHkAHqLaUImz/lwhsL/V499zXZ2gEmf\noKCacroXNDM85xUDAQgHwmEEGBMIABMFAlYxE9sJEMKxI4m0AaQ9AhsMAADk\ngwEA4B3lysFe/3+KE/PgCSZkUfx7n7xlKqMiqrX+VNyPej8BAMQJgtMVdslQ\nHLr5fhoGnRots3JSC0j20UQQOKVOXaW3\n=VpL9\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwV4DzYfy+90rz7YSAQdAWBKmhkgx+WtyERt903WpExRZfyAhxji8FKhthTRI\nLw4w/vzk9zMULlXZSknznkPnRlJyFUHqH9gFt8e3EQlij62Kd5T5AQBc0CLC\nfLZzou7u0sADAdWRcTHqdoIZCDgbJQEW3kbZOP9kEliyrB2K1UYYgOnGyLe2\nxpZd5f14Lfieb/CyO+BoqCpRSKJFSuMo7V+MY/mpt/liEMDPr+aRsOlyf+KE\ni9OznbgOX1oXdHbyodjTe9H7OxgFi/BxH6zQlFhxappmD5I3fmp1/ONvfXf/\nKN5U4Rx7ftKgsaTMsEnKk/w8rEqxL8a1YtLe4X1tdecRBTi7qbndYeXp5lVl\n+tnmmywmjXk+mx/l+NluOga79j7pZ0zO\n=Co9x\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73", + "name": "ed25519", + "password": "sun", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlIYEWkN+5BYJKwYBBAHaRw8BAQdAIGqj23Kp273IPkgjwA7ue5MDIRAfWLYRqnFy\nc2AFMcD+BwMCeaL+cNXzgI7uJQ7HBv53TAXO3y5uyJQMonkFtQtldL8YDbNP3pbd\n3zzo9fxU12bWAJyFwBlBWJqkrxZN+0jt0ElsG3kp+V67MESJkrRhKrQRTGlnaHQg\nPGxpZ2h0QHN1bj6IkAQTFggAOBYhBIZLQa5UQtPdGzTChx7N8CbAJFgwBQJaQ37k\nAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEB7N8CbAJFgwu14BAIVlyXyQ\nHEIfzrCaqlzb4q0QDyX+Gx+HuN34la7IW65EAPsE6LHxWjU/+G/ypRjBKfchFc94\nZe5yhZWgoaDFF37MA5yLBFpDfuQSCisGAQQBl1UBBQEBB0AvUqQcIXG0rUKC9mUi\ndP/Lo5B4J9eJjN3+Ljna3ZlHYAMBCAf+BwMCvyW2D5Yx6dbujE3yHi1XQ9MbhOY5\nXRFFgYIUYzzi1qmaL+8Gr9zODsUdeO60XHnMXOmqVa6/sdx32TWo5s3sgS19kRUM\nD+pbxS/aZnxvrYh4BBgWCAAgFiEEhktBrlRC090bNMKHHs3wJsAkWDAFAlpDfuQC\nGwwACgkQHs3wJsAkWDCe9QEA5qEE4N+qX465fNrK0ulz0ScZd6/+paVhmjYo9Fab\nQdsBANlddInzoZ8CCwsNagZXujp+2gWtue5axTPnDkjGhLIK\n=wo91\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEWkN+5BYJKwYBBAHaRw8BAQdAIGqj23Kp273IPkgjwA7ue5MDIRAfWLYRqnFy\nc2AFMcC0EUxpZ2h0IDxsaWdodEBzdW4+iJAEExYIADgWIQSGS0GuVELT3Rs0woce\nzfAmwCRYMAUCWkN+5AIbAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRAezfAm\nwCRYMLteAQCFZcl8kBxCH86wmqpc2+KtEA8l/hsfh7jd+JWuyFuuRAD7BOix8Vo1\nP/hv8qUYwSn3IRXPeGXucoWVoKGgxRd+zAO4OARaQ37kEgorBgEEAZdVAQUBAQdA\nL1KkHCFxtK1CgvZlInT/y6OQeCfXiYzd/i452t2ZR2ADAQgHiHgEGBYIACAWIQSG\nS0GuVELT3Rs0wocezfAmwCRYMAUCWkN+5AIbDAAKCRAezfAmwCRYMJ71AQDmoQTg\n36pfjrl82srS6XPRJxl3r/6lpWGaNij0VptB2wEA2V10ifOhnwILCw1qBle6On7a\nBa257lrFM+cOSMaEsgo=\n=D8HS\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nhH4DLFUmpfJ2kxcSAgMElfY0YbA1dI8s8MMhBHOXw0wwR/O+S8Pm/huBbkIbOb2c\nAL7ImXZYvPgS5tkpbxmItEedlLF439E8rwrPBqmrWTDSy/q9CyR2IKVSVNbConaz\nlyGGvVXNmGZm1jH2tDKAxqSGMUtuz4x6rgSqThRplSrSwBcBLd8NKo+3Q04AlSVf\nMdX0IZ3iualEff4RzpAwKdNO7V/y3z4Syhs2ZfXNGvt+F5Hnr9+PWnUcjQWUeWxS\nZ81hIqHWQ6paPBkM05I1P+zWuYF56UK7DGBIASBIJXaPclK1YoQ+o9ceChk9T6uy\nu3mju+/L4V2CAoY+G8DX3PtU6eFc+wVoyKFdEE9sjkVTbjK1zYqjlTR4R0zckX+L\ndiQ5YxtNVswo92sg9wpD93+EN7YSFx9ZlC7JrRSlf0betdX4iQ4VO8lG2FahgcYI\nwyZAsOwGJD4+TQ==\n=GeXG\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", + "name": "brainpoolp256r1", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlHgEWrpD4RMJKyQDAwIIAQEHAgMEMyiJsl3MxlZFRRg518IiUbv+/294KU+dBq/B\nQYbvt4dHh4M7O9Rgfic8EPbe47wKr6v6Z7wXgHpjqtKRoBzlxAAA/jhgOEGBKP4E\nIRHwnwkFI5FRIf6A+R9oZQ0kay8/+xNCDGy0HXRlc3RAZ29jcnlwdG8gPHRlc3RA\nZ29jcnlwdG8+iJAEExMIADgWIQTWZQHprbRVRe9D5xkELYRs3suJpAUCWrpD4QIb\nAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRAELYRs3suJpGVgAP48h9xGECGF\nL87pAJSRedg//d+ucwcWTRpblHskV59+HgD/T5DvU4AfmKvYUyhfkhLkqU+g2HO+\ndGU+/maeQymZBmycfARaukPhEgkrJAMDAggBAQcCAwQfD3jLYinp+eI5OjtwPhPs\nGYO/OtUhD8T9dyVJL306RgZyrtO2lwwEM3S/bN5F0lymte5XddH2wkt6Gv3stGV2\nAwEIBwAA/0vEs+P6LCQMFoxePlHi5kNvbuX0UH8YK8dAtsiY/LC3EdyIeAQYEwgA\nIBYhBNZlAemttFVF70PnGQQthGzey4mkBQJaukPhAhsMAAoJEAQthGzey4mk44wA\n/0CN2zUDoUEeVN1XouqbKgr9AxG/ffrZkvZDt4irMycOAP40NcvlLSUHrO6XQZPA\ngTHjrXXT++KbkzQRVWMO8UpRwg==\n=BkRB\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmFMEWrpD4RMJKyQDAwIIAQEHAgMEMyiJsl3MxlZFRRg518IiUbv+/294KU+dBq/B\nQYbvt4dHh4M7O9Rgfic8EPbe47wKr6v6Z7wXgHpjqtKRoBzlxLQddGVzdEBnb2Ny\neXB0byA8dGVzdEBnb2NyeXB0bz6IkAQTEwgAOBYhBNZlAemttFVF70PnGQQthGze\ny4mkBQJaukPhAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEAQthGzey4mk\nZWAA/jyH3EYQIYUvzukAlJF52D/9365zBxZNGluUeyRXn34eAP9PkO9TgB+Yq9hT\nKF+SEuSpT6DYc750ZT7+Zp5DKZkGbLhXBFq6Q+ESCSskAwMCCAEBBwIDBB8PeMti\nKen54jk6O3A+E+wZg7861SEPxP13JUkvfTpGBnKu07aXDAQzdL9s3kXSXKa17ld1\n0fbCS3oa/ey0ZXYDAQgHiHgEGBMIACAWIQTWZQHprbRVRe9D5xkELYRs3suJpAUC\nWrpD4QIbDAAKCRAELYRs3suJpOOMAP9Ajds1A6FBHlTdV6LqmyoK/QMRv3362ZL2\nQ7eIqzMnDgD+NDXL5S0lB6zul0GTwIEx46110/vim5M0EVVjDvFKUcI=\n=Bx7J\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nhJ4D25491by3UQcSAwMEidpiLdDr/FBd9HVhN1kjJkagjbXQrKPuu47ws2k67MBS\ngNo913vEQOzhqdiVliYtMAIpEt4sNyWCQ+TUEigsFiaG6Dp0wPG4/qVhRgRB4poN\njyIOvcTkW+Ze8m5wjUHJMIuUlrcUhDKpTRUiWaCuMG099YhTndV/vHHLJLAgyn3r\nGTmT6CnwHwtrbikJAC/I09LAOgGlDrS761lbkUbWm64Zl6Tqa56ioZVe1rEXB1aG\nx8uoraH9XwrKBu99KBnXegez3NBmb8V9r1ZJ6WTYN8QnIrtZCwI2ssdNLuQRZJU3\nX/D1ZeEkQO/125Df/df5vfR9J7Vmg5R1cJ03AEv4apu3P+P8PdvONF0Xr82ZX7++\nsLfK22tMrP1W0+mxnvu45B9Gz7ymT+2kLu6iyuvAW17IkC0yUqn7Js6XdDpFk/h7\nu5G2RTWbD9Zhdmv0p8MqpvTb8DugOTjwXxWa6Mr384O0btojdAV2+T3FBko45ubV\nbmKARW86AH7YpKjCbedsy9e5SvQohv102w/mZVk=\n=C5uY\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", + "name": "brainpoolp384r1", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlKgEWrqJ2RMJKyQDAwIIAQELAwMELWPYFx5PjaQLkP/dNEmMYqD72jsx/IzSO9j1\nFsmwE7hmosHMjXcDWrsxDuRqPfFMN98P/8kRB4Qn+o2dNHHdRuzAm/O5XCpoFGRL\nPJ7nIFn/DMf6gmFUxDZYwxbWiK2QAAF+NpeKlJvJr2gDf/d2DaRjwOCXCjUnOQuo\n+XQAKlEp8SG3LRhnD9E/bKG4q3JHcH1jFY+0InRlc3QzODRAZ29jcnlwdG8gPHRl\nc3QzODRAZ29jcnlwdD6IsAQTEwkAOBYhBPD66ZJLMNNU+RLWJOJ39qEi22ymBQJa\nuonZAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEOJ39qEi22ym1dwBfi1T\ne1FIGoDOWl6pFY1n92Fuur4m5gJKiZ+f1Sh2ZsyE8mfxqloSZsERKx4KEVZCgwF8\nDHhvCatkh/WrEE2RXGO9fRnLXZGKGYbwuXAXDFa/cJJ+bUIDu/epm/4ge7E+X7NW\nnKwEWrqJ2RIJKyQDAwIIAQELAwMEh3cON/Xg5LSr6T9iMqgtVEIW8eAoCfi+p8NS\n4Xr+OLcEX6PhcY5lTKUZEf0zBqVMAn4yw12W389gJEzcwEq+eoyG5FYetezdhC8Z\ne+df1Ha7nnLY0TWBjTvNEnxdgx2YAwEJCQABf2uXve5jHifo/pNRLy7vXFNYN2zJ\nw7GSYvY35hmx5OjiM8HkDSYom1UB125p1b+/EhmIiJgEGBMJACAWIQTw+umSSzDT\nVPkS1iTid/ahIttspgUCWrqJ2QIbDAAKCRDid/ahIttspozuAX437euAjbm+goCs\nbWXr9j8+oRRK56CODQrwjGCdjeyFP/wEHjV96ZbBBLAspykPwL0Bf3FgnJe/mxFU\nMfjeXWX2rg7rPiWO9HU41dsEcZ2pvN3sC5mQchfqivFINTvIngmk2g==\n=8J0V\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmHMEWrqJ2RMJKyQDAwIIAQELAwMELWPYFx5PjaQLkP/dNEmMYqD72jsx/IzSO9j1\nFsmwE7hmosHMjXcDWrsxDuRqPfFMN98P/8kRB4Qn+o2dNHHdRuzAm/O5XCpoFGRL\nPJ7nIFn/DMf6gmFUxDZYwxbWiK2QtCJ0ZXN0Mzg0QGdvY3J5cHRvIDx0ZXN0Mzg0\nQGdvY3J5cHQ+iLAEExMJADgWIQTw+umSSzDTVPkS1iTid/ahIttspgUCWrqJ2QIb\nAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRDid/ahIttsptXcAX4tU3tRSBqA\nzlpeqRWNZ/dhbrq+JuYCSomfn9UodmbMhPJn8apaEmbBESseChFWQoMBfAx4bwmr\nZIf1qxBNkVxjvX0Zy12RihmG8LlwFwxWv3CSfm1CA7v3qZv+IHuxPl+zVrh3BFq6\nidkSCSskAwMCCAEBCwMDBId3Djf14OS0q+k/YjKoLVRCFvHgKAn4vqfDUuF6/ji3\nBF+j4XGOZUylGRH9MwalTAJ+MsNdlt/PYCRM3MBKvnqMhuRWHrXs3YQvGXvnX9R2\nu55y2NE1gY07zRJ8XYMdmAMBCQmImAQYEwkAIBYhBPD66ZJLMNNU+RLWJOJ39qEi\n22ymBQJauonZAhsMAAoJEOJ39qEi22ymjO4Bfjft64CNub6CgKxtZev2Pz6hFErn\noI4NCvCMYJ2N7IU//AQeNX3plsEEsCynKQ/AvQF/cWCcl7+bEVQx+N5dZfauDus+\nJY70dTjV2wRxnam83ewLmZByF+qK8Ug1O8ieCaTa\n=vEH0\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nhL4DpJcoPAigmZESBAMEonrXiHjcMR/PE/ZwHEfC2rqhzugPOjxoytUCFx/WwLyI\nhREwlk3QA4wKO/xM9bgIkUg9bVlbJtsGceAcDgzxPonaeP+UhEMpi+otV4NT9y/F\nDPtdGCzM+n4rYHUzJqC5reyzjDbadiiV81YKoY67ZPulMwpqvZbCq1z9p0Qvpyww\nM4Tk2YJP1seL0WlcM3Tt7i0ZmegAa53OopqNLz5Mopba+f1aQOJeg8poY7xeR6Rq\n0sBaAf1vfSsrWQgBFxYwTWLtzysuls4l2h/xGoa2Ril9kAz0AZ7I3XdrXiaSkPAJ\nwkOI7zAdKE9urG0ZqsomInQq+u3RPQ349zms7B/+VttAlAg8WDKMDcNcFvPFDN85\nUACHO5UciN3682snpVmETKIo7jFXHj4ie9ITzpk/3MmWxvqbe1MiS1lQrRMY5PWL\nj9JULeBESodavfg2Mcf5tWGS7bMn/shojxbaYORZi70dw0Cl/e9Noh1hUeBd6ip6\nVkwhyLHSTB5tIq5pKSlNhgkuJDEhVvHsbFRjoioetH7hRkV4yolSWxB5iMT0lD1+\nKEKKTcNt1VHJ1cgSKi+S/fGYvHIPt1C2mXK1EBOqzJhVJxR8GNF3jF2aYG2r\n=zG+7\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", + "name": "brainpoolp512r1", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlNgEWrqKjRMJKyQDAwIIAQENBAMEDI4HTYTe2L0kzVVIJUrN7+8KivNLQNRUeLFp\noeKHeEqZdzv2zuYsqW91wTcxuobHuyhz2Rw/6GuxCjHMZKe1OEKzO73MHvk5x3Ut\ngsU3ziA4GYM1fPJobOJXwEvB7Hdt1jPnX6FDzsnnDb/V08o7oRDiFF+nakbZ4fdh\nBGPtoO8AAf94HXntn7GFtPoPKEG4GjDchW7kLoi99khL4afe/rEuuWDT4ue/osUw\ngDxddaBV2HpYlJtljbALvS+Fqlaz6+7mJOG0InRlc3Q1MTJAZ29jcnlwdG8gPHRl\nc3Q1MTJAZ29jcnlwdD6I0AQTEwoAOBYhBFZP0ImPk6gNK4o9sl/7WVyCi79OBQJa\nuoqNAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEF/7WVyCi79OH2YCAIdC\nLCIm4/8dTT3RROYFK/ERghCXH6Q42U5ojGdVc4+HSLiWyUijjqSznhrRtY4Izfcd\n23eLZCo1TjvfPWsK8N0B/1eRbHersapjqA2jhHjbhGr6pyZhmBZtMpeqJ80R86Ka\ncFcRH008VrqJoZwSRt+BQHDdwiWG+ZQiTY8Y9KmZ5pSc3ARauoqNEgkrJAMDAggB\nAQ0EAwQ0T5L5YcJMUFF8OYFvHlwcym3o3APGsbUCvfhUguYWOyEr9drNiYCaC0gl\nbO+KackLO30gWiVg1gHY3CoJfi0kRwt1tXKkiqh7wCD2BPDzP0lhF3nHaAKNpg3h\nmpGUcZui9FyTezwK6CjqBgAhLETCwevB1MXQuvvPgw9uil7rbQMBCgkAAf96G+z/\nK9vrz/nhvZessIeGAEHhmrGkHLBpw+9IGDs7hWNvisTn99JezTtPbYMInrwmEOB9\n9VoAIO8Xrom5XFXjI42IuAQYEwoAIBYhBFZP0ImPk6gNK4o9sl/7WVyCi79OBQJa\nuoqNAhsMAAoJEF/7WVyCi79O3QIB/1e/HKOHV7504x0wu14qXDN+2QW8P6j8d0qI\nGB7xOegf8Z8KgzGywZFjTT6GKBqPTz2vMd4u44/sLBVBgPgiKQgB/308ETfQaPcz\nctjlrmykdX0TrdiKLy92xAqsohFff5Ri5pr500005rTYJfNYN+Cug6u9UygWL2RY\nu4H95mtsxZo=\n=Qb7k\n-----END PGP PRIVATE KEY BLOCK-----", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmJMEWrqKjRMJKyQDAwIIAQENBAMEDI4HTYTe2L0kzVVIJUrN7+8KivNLQNRUeLFp\noeKHeEqZdzv2zuYsqW91wTcxuobHuyhz2Rw/6GuxCjHMZKe1OEKzO73MHvk5x3Ut\ngsU3ziA4GYM1fPJobOJXwEvB7Hdt1jPnX6FDzsnnDb/V08o7oRDiFF+nakbZ4fdh\nBGPtoO+0InRlc3Q1MTJAZ29jcnlwdG8gPHRlc3Q1MTJAZ29jcnlwdD6I0AQTEwoA\nOBYhBFZP0ImPk6gNK4o9sl/7WVyCi79OBQJauoqNAhsDBQsJCAcCBhUICQoLAgQW\nAgMBAh4BAheAAAoJEF/7WVyCi79OH2YCAIdCLCIm4/8dTT3RROYFK/ERghCXH6Q4\n2U5ojGdVc4+HSLiWyUijjqSznhrRtY4Izfcd23eLZCo1TjvfPWsK8N0B/1eRbHer\nsapjqA2jhHjbhGr6pyZhmBZtMpeqJ80R86KacFcRH008VrqJoZwSRt+BQHDdwiWG\n+ZQiTY8Y9KmZ5pS4lwRauoqNEgkrJAMDAggBAQ0EAwQ0T5L5YcJMUFF8OYFvHlwc\nym3o3APGsbUCvfhUguYWOyEr9drNiYCaC0glbO+KackLO30gWiVg1gHY3CoJfi0k\nRwt1tXKkiqh7wCD2BPDzP0lhF3nHaAKNpg3hmpGUcZui9FyTezwK6CjqBgAhLETC\nwevB1MXQuvvPgw9uil7rbQMBCgmIuAQYEwoAIBYhBFZP0ImPk6gNK4o9sl/7WVyC\ni79OBQJauoqNAhsMAAoJEF/7WVyCi79O3QIB/1e/HKOHV7504x0wu14qXDN+2QW8\nP6j8d0qIGB7xOegf8Z8KgzGywZFjTT6GKBqPTz2vMd4u44/sLBVBgPgiKQgB/308\nETfQaPczctjlrmykdX0TrdiKLy92xAqsohFff5Ri5pr500005rTYJfNYN+Cug6u9\nUygWL2RYu4H95mtsxZo=\n=3o0a\n-----END PGP PUBLIC KEY BLOCK-----" + } +] diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go new file mode 100644 index 000000000..0c3c49c31 --- /dev/null +++ b/openpgp/integration_tests/v2/utils_test.go @@ -0,0 +1,306 @@ +package v2 + +import ( + "bytes" + "crypto" + "crypto/rand" + mathrand "math/rand" + "strings" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" +) + +// This function produces random test vectors: generates keys according to the +// given settings, associates a random message for each key. It returns the +// test vectors. +func generateFreshTestVectors(num int) (vectors []testVector, err error) { + mathrand.Seed(time.Now().UTC().UnixNano()) + for i := 0; i < num; i++ { + config := randConfig() + // Sample random email, comment, password and message + name, email, comment, password, message := randEntityData() + + // Only for verbose display + v := "v4" + if config.V6() { + v = "v6" + } + pkAlgoNames := map[packet.PublicKeyAlgorithm]string{ + packet.PubKeyAlgoRSA: "rsa_" + v, + packet.PubKeyAlgoEdDSA: "EdDSA_" + v, + packet.PubKeyAlgoEd25519: "ed25519_" + v, + packet.PubKeyAlgoEd448: "ed448_" + v, + } + + newVector := testVector{ + config: config, + Name: pkAlgoNames[config.Algorithm], + Password: password, + Message: message, + } + + // Generate keys + newEntity, errKG := openpgp.NewEntity(name, comment, email, config) + if errKG != nil { + panic(errKG) + } + + // Encrypt private key of entity + rawPwd := []byte(password) + if newEntity.PrivateKey != nil && !newEntity.PrivateKey.Encrypted { + if err = newEntity.PrivateKey.Encrypt(rawPwd); err != nil { + panic(err) + } + } + + // Encrypt subkeys of entity + for _, sub := range newEntity.Subkeys { + if sub.PrivateKey != nil && !sub.PrivateKey.Encrypted { + if err = sub.PrivateKey.Encrypt(rawPwd); err != nil { + panic(err) + } + } + } + + w := bytes.NewBuffer(nil) + if err = newEntity.SerializePrivateWithoutSigning(w, nil); err != nil { + return nil, err + } + + serialized := w.Bytes() + + privateKey, _ := armorWithType(serialized, "PGP PRIVATE KEY BLOCK") + newVector.PrivateKey = privateKey + newVector.PublicKey, _ = publicKey(privateKey) + vectors = append(vectors, newVector) + } + return vectors, err +} + +// armorWithType make bytes input to armor format +func armorWithType(input []byte, armorType string) (string, error) { + var b bytes.Buffer + w, err := armor.EncodeWithChecksumOption(&b, armorType, nil, false) + if err != nil { + return "", err + } + if _, err = w.Write(input); err != nil { + return "", err + } + if err := w.Close(); err != nil { + return "", err + } + return b.String(), nil +} + +func publicKey(privateKey string) (string, error) { + privKeyReader := strings.NewReader(privateKey) + entries, err := openpgp.ReadArmoredKeyRing(privKeyReader) + if err != nil { + return "", err + } + + var outBuf bytes.Buffer + for _, e := range entries { + err := e.Serialize(&outBuf) + if err != nil { + return "", err + } + } + + outString, err := armorWithType(outBuf.Bytes(), "PGP PUBLIC KEY BLOCK") + if err != nil { + return "", err + } + + return outString, nil +} + +var runes = []rune("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKMNOPQRSTUVWXYZ.@-_:;?/!#$%^&*{}[]'\"+~()<>") + +func randName() string { + firstName := make([]rune, 8) + lastName := make([]rune, 8) + nameRunes := runes[:26] + + for i := range firstName { + firstName[i] = nameRunes[mathrand.Intn(len(nameRunes))] + } + + for i := range lastName { + lastName[i] = nameRunes[mathrand.Intn(len(nameRunes))] + } + + return string(firstName) + " " + string(lastName) +} + +func randFileHints() *openpgp.FileHints { + fileNameRunes := runes[:66] + fileName := make([]rune, 1+mathrand.Intn(255)) + for i := range fileName { + fileName[i] = fileNameRunes[mathrand.Intn(len(fileNameRunes))] + } + + return &openpgp.FileHints{ + IsUTF8: mathrand.Intn(2) == 0, + FileName: string(fileName), + ModTime: time.Now(), + } +} + +func randEmail() string { + address := make([]rune, 20) + addressRunes := runes[:38] + domain := make([]rune, 5) + domainRunes := runes[:36] + ext := make([]rune, 3) + for i := range address { + address[i] = addressRunes[mathrand.Intn(len(addressRunes))] + } + for i := range domain { + domain[i] = domainRunes[mathrand.Intn(len(domainRunes))] + } + for i := range ext { + ext[i] = domainRunes[mathrand.Intn(len(domainRunes))] + } + email := string(address) + "@" + string(domain) + "." + string(ext) + return email +} + +// Comment does not allow the following characters: ()<>\x00 +func randComment() string { + comment := make([]rune, 140) + commentRunes := runes[:84] + for i := range comment { + comment[i] = commentRunes[mathrand.Intn(len(commentRunes))] + } + return string(comment) +} + +func randPassword() string { + maxPasswordLength := 64 + password := make([]rune, mathrand.Intn(maxPasswordLength-1)+1) + for i := range password { + password[i] = runes[mathrand.Intn(len(runes))] + } + return string(password) +} + +func randMessage() string { + maxMessageLength := 1 << 20 + message := make([]byte, 1+mathrand.Intn(maxMessageLength-1)) + if _, err := rand.Read(message); err != nil { + panic(err) + } + return string(message) +} + +// Change one char of the input +func corrupt(input string) string { + if input == "" { + return string(runes[mathrand.Intn(len(runes))]) + } + output := []rune(input) + for string(output) == input { + output[mathrand.Intn(len(output))] = runes[mathrand.Intn(len(runes))] + } + return string(output) +} + +func randEntityData() (string, string, string, string, string) { + return randName(), randEmail(), randComment(), randPassword(), randMessage() +} + +func randConfig() *packet.Config { + hashes := []crypto.Hash{ + crypto.SHA256, + } + hash := hashes[mathrand.Intn(len(hashes))] + + ciphers := []packet.CipherFunction{ + packet.CipherAES256, + } + ciph := ciphers[mathrand.Intn(len(ciphers))] + + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + + pkAlgos := []packet.PublicKeyAlgorithm{ + packet.PubKeyAlgoRSA, + packet.PubKeyAlgoEdDSA, + packet.PubKeyAlgoEd25519, + packet.PubKeyAlgoEd448, + } + pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] + + aeadModes := []packet.AEADMode{ + packet.AEADModeOCB, + packet.AEADModeEAX, + packet.AEADModeGCM, + } + var aeadConf = packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + + var rsaBits int + if pkAlgo == packet.PubKeyAlgoRSA { + switch mathrand.Int() % 4 { + case 0: + rsaBits = 2048 + case 1: + rsaBits = 3072 + case 2: + rsaBits = 4096 + default: + rsaBits = 0 + } + } + + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + + var v6 bool + if mathrand.Int()%2 == 0 { + v6 = true + if pkAlgo == packet.PubKeyAlgoEdDSA { + pkAlgo = packet.PubKeyAlgoEd25519 + } + } + + var s2kConf *s2k.Config + if mathrand.Int()%2 == 0 { + s2kConf = &s2k.Config{ + S2KMode: s2k.IteratedSaltedS2K, + Hash: hash, + S2KCount: 1024 + mathrand.Intn(65010689), + } + } else { + s2kConf = &s2k.Config{ + S2KMode: s2k.Argon2S2K, + } + } + + return &packet.Config{ + V6Keys: v6, + Rand: rand.Reader, + DefaultHash: hash, + DefaultCipher: ciph, + DefaultCompressionAlgo: compAlgo, + CompressionConfig: compConf, + S2KConfig: s2kConf, + RSABits: rsaBits, + Algorithm: pkAlgo, + AEADConfig: &aeadConf, + RejectPublicKeyAlgorithms: map[packet.PublicKeyAlgorithm]bool{}, + RejectCurves: map[packet.Curve]bool{}, + MinRSABits: 512, + } +} diff --git a/openpgp/internal/algorithm/cipher.go b/openpgp/internal/algorithm/cipher.go index 5760cff80..c76a75bcd 100644 --- a/openpgp/internal/algorithm/cipher.go +++ b/openpgp/internal/algorithm/cipher.go @@ -51,24 +51,14 @@ func (sk CipherFunction) Id() uint8 { return uint8(sk) } -var keySizeByID = map[uint8]int{ - TripleDES.Id(): 24, - CAST5.Id(): cast5.KeySize, - AES128.Id(): 16, - AES192.Id(): 24, - AES256.Id(): 32, -} - // KeySize returns the key size, in bytes, of cipher. func (cipher CipherFunction) KeySize() int { switch cipher { - case TripleDES: - return 24 case CAST5: return cast5.KeySize case AES128: return 16 - case AES192: + case AES192, TripleDES: return 24 case AES256: return 32 diff --git a/openpgp/internal/ecc/curve_info.go b/openpgp/internal/ecc/curve_info.go index 35751034d..97f891ffc 100644 --- a/openpgp/internal/ecc/curve_info.go +++ b/openpgp/internal/ecc/curve_info.go @@ -4,6 +4,7 @@ package ecc import ( "bytes" "crypto/elliptic" + "github.com/ProtonMail/go-crypto/bitcurves" "github.com/ProtonMail/go-crypto/brainpool" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" @@ -47,7 +48,7 @@ var Curves = []CurveInfo{ Curve: NewCurve25519(), }, { - // X448 + // x448 GenName: "Curve448", Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x6F}), Curve: NewX448(), diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 0e71934cd..a40e45bee 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -15,11 +15,15 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/ed25519" + "github.com/ProtonMail/go-crypto/openpgp/ed448" "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/x25519" + "github.com/ProtonMail/go-crypto/openpgp/x448" ) // NewEntity returns an Entity that contains a fresh RSA/RSA keypair with a @@ -36,8 +40,8 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err return nil, err } primary := packet.NewSignerPrivateKey(creationTime, primaryPrivRaw) - if config != nil && config.V5Keys { - primary.UpgradeToV5() + if config.V6() { + primary.UpgradeToV6() } e := &Entity{ @@ -45,9 +49,25 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err PrivateKey: primary, Identities: make(map[string]*Identity), Subkeys: []Subkey{}, + Signatures: []*packet.Signature{}, } - err = e.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs) + if config.V6() { + // In v6 keys algorithm preferences should be stored in direct key signatures + selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypeDirectSignature, config) + err = writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config) + if err != nil { + return nil, err + } + err = selfSignature.SignDirectKeyBinding(&primary.PublicKey, primary, config) + if err != nil { + return nil, err + } + e.Signatures = append(e.Signatures, selfSignature) + e.SelfSignature = selfSignature + } + + err = e.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs, !config.V6()) if err != nil { return nil, err } @@ -65,27 +85,12 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error { creationTime := config.Now() keyLifetimeSecs := config.KeyLifetime() - return t.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs) + return t.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs, !config.V6()) } -func (t *Entity) addUserId(name, comment, email string, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32) error { - uid := packet.NewUserId(name, comment, email) - if uid == nil { - return errors.InvalidArgumentError("user id field contained invalid characters") - } - - if _, ok := t.Identities[uid.Id]; ok { - return errors.InvalidArgumentError("user id exist") - } - - primary := t.PrivateKey - - isPrimaryId := len(t.Identities) == 0 - - selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config) +func writeKeyProperties(selfSignature *packet.Signature, creationTime time.Time, keyLifetimeSecs uint32, config *packet.Config) error { selfSignature.CreationTime = creationTime selfSignature.KeyLifetimeSecs = &keyLifetimeSecs - selfSignature.IsPrimaryId = &isPrimaryId selfSignature.FlagsValid = true selfSignature.FlagSign = true selfSignature.FlagCertify = true @@ -131,6 +136,29 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode}) } } + return nil +} + +func (t *Entity) addUserId(name, comment, email string, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32, writeProperties bool) error { + uid := packet.NewUserId(name, comment, email) + if uid == nil { + return errors.InvalidArgumentError("user id field contained invalid characters") + } + + if _, ok := t.Identities[uid.Id]; ok { + return errors.InvalidArgumentError("user id exist") + } + + primary := t.PrivateKey + isPrimaryId := len(t.Identities) == 0 + selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config) + if writeProperties { + err := writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config) + if err != nil { + return err + } + } + selfSignature.IsPrimaryId = &isPrimaryId // User ID binding signature err := selfSignature.SignUserId(uid.Id, &primary.PublicKey, primary, config) @@ -158,8 +186,8 @@ func (e *Entity) AddSigningSubkey(config *packet.Config) error { } sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw) sub.IsSubkey = true - if config != nil && config.V5Keys { - sub.UpgradeToV5() + if config.V6() { + sub.UpgradeToV6() } subkey := Subkey{ @@ -203,8 +231,8 @@ func (e *Entity) addEncryptionSubkey(config *packet.Config, creationTime time.Ti } sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw) sub.IsSubkey = true - if config != nil && config.V5Keys { - sub.UpgradeToV5() + if config.V6() { + sub.UpgradeToV6() } subkey := Subkey{ @@ -242,6 +270,11 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { } return rsa.GenerateKey(config.Random(), bits) case packet.PubKeyAlgoEdDSA: + if config.V6() { + // Implementations MUST NOT accept or generate v6 key material + // using the deprecated OIDs. + return nil, errors.InvalidArgumentError("EdDSALegacy cannot be used for v6 keys") + } curve := ecc.FindEdDSAByGenName(string(config.CurveName())) if curve == nil { return nil, errors.InvalidArgumentError("unsupported curve") @@ -263,6 +296,18 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { return nil, err } return priv, nil + case packet.PubKeyAlgoEd25519: + priv, err := ed25519.GenerateKey(config.Random()) + if err != nil { + return nil, err + } + return priv, nil + case packet.PubKeyAlgoEd448: + priv, err := ed448.GenerateKey(config.Random()) + if err != nil { + return nil, err + } + return priv, nil default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -285,6 +330,13 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA: fallthrough // When passing EdDSA or ECDSA, we generate an ECDH subkey case packet.PubKeyAlgoECDH: + if config.V6() && + (config.CurveName() == packet.Curve25519 || + config.CurveName() == packet.Curve448) { + // Implementations MUST NOT accept or generate v6 key material + // using the deprecated OIDs. + return nil, errors.InvalidArgumentError("ECDH with Curve25519/448 legacy cannot be used for v6 keys") + } var kdf = ecdh.KDF{ Hash: algorithm.SHA512, Cipher: algorithm.AES256, @@ -294,6 +346,10 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return nil, errors.InvalidArgumentError("unsupported curve") } return ecdh.GenerateKey(config.Random(), curve, kdf) + case packet.PubKeyAlgoEd25519, packet.PubKeyAlgoX25519: // When passing Ed25519, we generate an x25519 subkey + return x25519.GenerateKey(config.Random()) + case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey + return x448.GenerateKey(config.Random()) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -302,7 +358,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { var bigOne = big.NewInt(1) // generateRSAKeyWithPrimes generates a multi-prime RSA keypair of the -// given bit size, using the given random source and prepopulated primes. +// given bit size, using the given random source and pre-populated primes. func generateRSAKeyWithPrimes(random io.Reader, nprimes int, bits int, prepopulatedPrimes []*big.Int) (*rsa.PrivateKey, error) { priv := new(rsa.PrivateKey) priv.E = 65537 diff --git a/openpgp/keys.go b/openpgp/keys.go index 2d7b0cf37..a071353e2 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -6,6 +6,7 @@ package openpgp import ( goerrors "errors" + "fmt" "io" "time" @@ -24,11 +25,13 @@ var PrivateKeyType = "PGP PRIVATE KEY BLOCK" // (which must be a signing key), one or more identities claimed by that key, // and zero or more subkeys, which may be encryption keys. type Entity struct { - PrimaryKey *packet.PublicKey - PrivateKey *packet.PrivateKey - Identities map[string]*Identity // indexed by Identity.Name - Revocations []*packet.Signature - Subkeys []Subkey + PrimaryKey *packet.PublicKey + PrivateKey *packet.PrivateKey + Identities map[string]*Identity // indexed by Identity.Name + Revocations []*packet.Signature + Subkeys []Subkey + SelfSignature *packet.Signature // Direct-key self signature of the PrimaryKey (contains primary key properties in v6) + Signatures []*packet.Signature // all (potentially unverified) self-signatures, revocations, and third-party signatures } // An Identity represents an identity claimed by an Entity and zero or more @@ -120,12 +123,12 @@ func shouldPreferIdentity(existingId, potentialNewId *Identity) bool { // given Entity. func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { // Fail to find any encryption key if the... - i := e.PrimaryIdentity() - if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired - i.SelfSignature == nil || // user ID has no self-signature - i.SelfSignature.SigExpired(now) || // user ID self-signature has expired + primarySelfSignature, primaryIdentity := e.PrimarySelfSignature() + if primarySelfSignature == nil || // no self-signature found + e.PrimaryKey.KeyExpired(primarySelfSignature, now) || // primary key has expired e.Revoked(now) || // primary key has been revoked - i.Revoked(now) { // user ID has been revoked + primarySelfSignature.SigExpired(now) || // user ID or or direct self-signature has expired + (primaryIdentity != nil && primaryIdentity.Revoked(now)) { // user ID has been revoked (for v4 keys) return Key{}, false } @@ -152,9 +155,9 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { // If we don't have any subkeys for encryption and the primary key // is marked as OK to encrypt with, then we can use it. - if i.SelfSignature.FlagsValid && i.SelfSignature.FlagEncryptCommunications && + if primarySelfSignature.FlagsValid && primarySelfSignature.FlagEncryptCommunications && e.PrimaryKey.PubKeyAlgo.CanEncrypt() { - return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true + return Key{e, e.PrimaryKey, e.PrivateKey, primarySelfSignature, e.Revocations}, true } return Key{}, false @@ -186,12 +189,12 @@ func (e *Entity) SigningKeyById(now time.Time, id uint64) (Key, bool) { func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, bool) { // Fail to find any signing key if the... - i := e.PrimaryIdentity() - if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired - i.SelfSignature == nil || // user ID has no self-signature - i.SelfSignature.SigExpired(now) || // user ID self-signature has expired + primarySelfSignature, primaryIdentity := e.PrimarySelfSignature() + if primarySelfSignature == nil || // no self-signature found + e.PrimaryKey.KeyExpired(primarySelfSignature, now) || // primary key has expired e.Revoked(now) || // primary key has been revoked - i.Revoked(now) { // user ID has been revoked + primarySelfSignature.SigExpired(now) || // user ID or direct self-signature has expired + (primaryIdentity != nil && primaryIdentity.Revoked(now)) { // user ID has been revoked (for v4 keys) return Key{}, false } @@ -220,12 +223,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, // If we don't have any subkeys for signing and the primary key // is marked as OK to sign with, then we can use it. - if i.SelfSignature.FlagsValid && - (flags&packet.KeyFlagCertify == 0 || i.SelfSignature.FlagCertify) && - (flags&packet.KeyFlagSign == 0 || i.SelfSignature.FlagSign) && + if primarySelfSignature.FlagsValid && + (flags&packet.KeyFlagCertify == 0 || primarySelfSignature.FlagCertify) && + (flags&packet.KeyFlagSign == 0 || primarySelfSignature.FlagSign) && e.PrimaryKey.PubKeyAlgo.CanSign() && (id == 0 || e.PrimaryKey.KeyId == id) { - return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true + return Key{e, e.PrimaryKey, e.PrivateKey, primarySelfSignature, e.Revocations}, true } // No keys with a valid Signing Flag or no keys matched the id passed in @@ -259,7 +262,7 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er var keysToEncrypt []*packet.PrivateKey // Add entity private key to encrypt. if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted { - keysToEncrypt = append(keysToEncrypt, e.PrivateKey) + keysToEncrypt = append(keysToEncrypt, e.PrivateKey) } // Add subkeys to encrypt. @@ -271,7 +274,7 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er return packet.EncryptPrivateKeys(keysToEncrypt, passphrase, config) } -// DecryptPrivateKeys decrypts all encrypted keys in the entitiy with the given passphrase. +// DecryptPrivateKeys decrypts all encrypted keys in the entity with the given passphrase. // Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored, // and don't cause an error to be returned. func (e *Entity) DecryptPrivateKeys(passphrase []byte) error { @@ -284,7 +287,7 @@ func (e *Entity) DecryptPrivateKeys(passphrase []byte) error { // Add subkeys to decrypt. for _, sub := range e.Subkeys { if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && sub.PrivateKey.Encrypted { - keysToDecrypt = append(keysToDecrypt, sub.PrivateKey) + keysToDecrypt = append(keysToDecrypt, sub.PrivateKey) } } return packet.DecryptPrivateKeys(keysToDecrypt, passphrase) @@ -318,8 +321,7 @@ type EntityList []*Entity func (el EntityList) KeysById(id uint64) (keys []Key) { for _, e := range el { if e.PrimaryKey.KeyId == id { - ident := e.PrimaryIdentity() - selfSig := ident.SelfSignature + selfSig, _ := e.PrimarySelfSignature() keys = append(keys, Key{e, e.PrimaryKey, e.PrivateKey, selfSig, e.Revocations}) } @@ -441,7 +443,6 @@ func readToNextPublicKey(packets *packet.Reader) (err error) { return } else if err != nil { if _, ok := err.(errors.UnsupportedError); ok { - err = nil continue } return @@ -479,6 +480,7 @@ func ReadEntity(packets *packet.Reader) (*Entity, error) { } var revocations []*packet.Signature + var directSignatures []*packet.Signature EachPacket: for { p, err := packets.Next() @@ -497,9 +499,7 @@ EachPacket: if pkt.SigType == packet.SigTypeKeyRevocation { revocations = append(revocations, pkt) } else if pkt.SigType == packet.SigTypeDirectSignature { - // TODO: RFC4880 5.2.1 permits signatures - // directly on keys (eg. to bind additional - // revocation keys). + directSignatures = append(directSignatures, pkt) } // Else, ignoring the signature as it does not follow anything // we would know to attach it to. @@ -522,12 +522,39 @@ EachPacket: return nil, err } default: - // we ignore unknown packets + // we ignore unknown packets. } } - if len(e.Identities) == 0 { - return nil, errors.StructuralError("entity without any identities") + if len(e.Identities) == 0 && e.PrimaryKey.Version < 6 { + return nil, errors.StructuralError(fmt.Sprintf("v%d entity without any identities", e.PrimaryKey.Version)) + } + + // An implementation MUST ensure that a valid direct-key signature is present before using a v6 key. + if e.PrimaryKey.Version == 6 { + if len(directSignatures) == 0 { + return nil, errors.StructuralError("v6 entity without a valid direct-key signature") + } + // Select main direct key signature. + var mainDirectKeySelfSignature *packet.Signature + for _, directSignature := range directSignatures { + if directSignature.SigType == packet.SigTypeDirectSignature && + directSignature.CheckKeyIdOrFingerprint(e.PrimaryKey) && + (mainDirectKeySelfSignature == nil || + directSignature.CreationTime.After(mainDirectKeySelfSignature.CreationTime)) { + mainDirectKeySelfSignature = directSignature + } + } + if mainDirectKeySelfSignature == nil { + return nil, errors.StructuralError("no valid direct-key self-signature for v6 primary key found") + } + // Check that the main self-signature is valid. + err = e.PrimaryKey.VerifyDirectKeySignature(mainDirectKeySelfSignature) + if err != nil { + return nil, errors.StructuralError("invalid direct-key self-signature for v6 primary key") + } + e.SelfSignature = mainDirectKeySelfSignature + e.Signatures = directSignatures } for _, revocation := range revocations { @@ -672,6 +699,12 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo return err } } + for _, directSignature := range e.Signatures { + err := directSignature.Serialize(w) + if err != nil { + return err + } + } for _, ident := range e.Identities { err = ident.UserId.Serialize(w) if err != nil { @@ -738,6 +771,12 @@ func (e *Entity) Serialize(w io.Writer) error { return err } } + for _, directSignature := range e.Signatures { + err := directSignature.Serialize(w) + if err != nil { + return err + } + } for _, ident := range e.Identities { err = ident.UserId.Serialize(w) if err != nil { @@ -840,3 +879,23 @@ func (e *Entity) RevokeSubkey(sk *Subkey, reason packet.ReasonForRevocation, rea sk.Revocations = append(sk.Revocations, revSig) return nil } + +func (e *Entity) primaryDirectSignature() *packet.Signature { + return e.SelfSignature +} + +// PrimarySelfSignature searches the entity for the self-signature that stores key preferences. +// For V4 keys, returns the self-signature of the primary identity, and the identity. +// For V6 keys, returns the latest valid direct-key self-signature, and no identity (nil). +// This self-signature is to be used to check the key expiration, +// algorithm preferences, and so on. +func (e *Entity) PrimarySelfSignature() (*packet.Signature, *Identity) { + if e.PrimaryKey.Version == 6 { + return e.primaryDirectSignature(), nil + } + primaryIdentity := e.PrimaryIdentity() + if primaryIdentity == nil { + return nil, nil + } + return primaryIdentity.SelfSignature, primaryIdentity +} diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 35bb495b9..bd0bf322d 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -1081,7 +1081,9 @@ func TestAddUserId(t *testing.T) { } serializedEntity := bytes.NewBuffer(nil) - entity.SerializePrivate(serializedEntity, nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) if err != nil { @@ -1116,7 +1118,9 @@ func TestAddSubkey(t *testing.T) { } serializedEntity := bytes.NewBuffer(nil) - entity.SerializePrivate(serializedEntity, nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) if err != nil { @@ -1141,7 +1145,9 @@ func TestAddSubkeySerialized(t *testing.T) { } serializedEntity := bytes.NewBuffer(nil) - entity.SerializePrivateWithoutSigning(serializedEntity, nil) + if err = entity.SerializePrivateWithoutSigning(serializedEntity, nil); err != nil { + t.Fatal(err) + } entity, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) if err != nil { @@ -1217,7 +1223,9 @@ func TestAddSubkeyWithConfig(t *testing.T) { } serializedEntity := bytes.NewBuffer(nil) - entity.SerializePrivate(serializedEntity, nil) + if err = entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) if err != nil { @@ -1246,7 +1254,9 @@ func TestAddSubkeyWithConfigSerialized(t *testing.T) { } serializedEntity := bytes.NewBuffer(nil) - entity.SerializePrivateWithoutSigning(serializedEntity, nil) + if err := entity.SerializePrivateWithoutSigning(serializedEntity, nil); err != nil { + t.Fatal(err) + } entity, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) if err != nil { @@ -1378,7 +1388,9 @@ func TestRevokeSubkey(t *testing.T) { } serializedEntity := bytes.NewBuffer(nil) - entity.SerializePrivate(serializedEntity, nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } // Make sure revocation reason subpackets are not lost during serialization. newEntity, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) @@ -1489,7 +1501,7 @@ func TestEncryptAndDecryptPrivateKeys(t *testing.T) { if err != nil { t.Fatal(err) } - + if !entity.PrivateKey.Encrypted { t.Fatal("Expected encrypted private key") } @@ -1498,12 +1510,12 @@ func TestEncryptAndDecryptPrivateKeys(t *testing.T) { t.Fatal("Expected encrypted private key") } } - + err = entity.DecryptPrivateKeys(passphrase) if err != nil { t.Fatal(err) } - + if entity.PrivateKey.Encrypted { t.Fatal("Expected plaintext private key") } @@ -1514,8 +1526,6 @@ func TestEncryptAndDecryptPrivateKeys(t *testing.T) { } }) } - - } func TestKeyValidateOnDecrypt(t *testing.T) { @@ -1796,6 +1806,47 @@ func testKeyValidateDsaElGamalOnDecrypt(t *testing.T, randomPassword []byte) { } } +var foreignKeysv4 = []string{ + v4Key25519, +} + +func TestReadPrivateForeignV4Key(t *testing.T) { + for _, str := range foreignKeysv4 { + kring, err := ReadArmoredKeyRing(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + checkV4Key(t, kring[0]) + } +} + +func checkV4Key(t *testing.T, ent *Entity) { + key := ent.PrimaryKey + if key.Version != 4 { + t.Errorf("wrong key version %d", key.Version) + } + if len(key.Fingerprint) != 20 { + t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) + } + signatures := ent.Revocations + for _, id := range ent.Identities { + signatures = append(signatures, id.SelfSignature) + signatures = append(signatures, id.Signatures...) + } + for _, sig := range signatures { + if sig == nil { + continue + } + if sig.Version != 4 { + t.Errorf("wrong signature version %d", sig.Version) + } + fgptLen := len(sig.IssuerFingerprint) + if fgptLen != 20 { + t.Errorf("Wrong fingerprint length in signature: %d", fgptLen) + } + } +} + // Should not panic (generated with go-fuzz) func TestCorruptKeys(t *testing.T) { data := `-----BEGIN PGP PUBLIC KEY BLOCK00000 diff --git a/openpgp/keys_v5_test.go b/openpgp/keys_v5_test.go index 8a38bd902..36b83a4a7 100644 --- a/openpgp/keys_v5_test.go +++ b/openpgp/keys_v5_test.go @@ -2,12 +2,8 @@ package openpgp import ( "bytes" - "io/ioutil" "strings" "testing" - - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" ) var foreignKeys = []string{ @@ -24,87 +20,6 @@ func TestReadPrivateForeignV5Key(t *testing.T) { } } -// TODO: Replace message with a correctly generated one. -func testV5ForeignSignedMessage(t *testing.T) { - kring, err := ReadArmoredKeyRing(strings.NewReader(v5PrivKey)) - if err != nil { - t.Fatal(err) - } - msg := strings.NewReader(v5PrivKeyMsg) - // Unarmor - block, err := armor.Decode(msg) - if err != nil { - return - } - md, err := ReadMessage(block.Body, kring, nil, nil) - if md.SignedBy == nil { - t.Fatal("incorrect signer") - } - if err != nil { - t.Fatal(err) - } - // Consume UnverifiedBody - body, err := ioutil.ReadAll(md.UnverifiedBody) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(body, []byte("test")) { - t.Fatal("bad body") - } - if md.SignatureError != nil { - t.Fatal(md.SignatureError) - } - if err != nil { - t.Fatal(err) - } -} - -func TestReadPrivateEncryptedV5Key(t *testing.T) { - c := &packet.Config{V5Keys: true} - e, err := NewEntity("V5 Key Owner", "V5 Key", "v5@pm.me", c) - if err != nil { - t.Fatal(err) - } - password := []byte("test v5 key # password") - // Encrypt private key - if err = e.PrivateKey.Encrypt(password); err != nil { - t.Fatal(err) - } - // Encrypt subkeys - for _, sub := range e.Subkeys { - if err = sub.PrivateKey.Encrypt(password); err != nil { - t.Fatal(err) - } - } - // Serialize, Read - serializedEntity := bytes.NewBuffer(nil) - err = e.SerializePrivateWithoutSigning(serializedEntity, nil) - if err != nil { - t.Fatal(err) - } - el, err := ReadKeyRing(serializedEntity) - if err != nil { - t.Fatal(err) - } - - // Decrypt - if el[0].PrivateKey == nil { - t.Fatal("No private key found") - } - if err = el[0].PrivateKey.Decrypt(password); err != nil { - t.Error(err) - } - - // Decrypt subkeys - for _, sub := range e.Subkeys { - if err = sub.PrivateKey.Decrypt(password); err != nil { - t.Error(err) - } - } - - checkV5Key(t, el[0]) -} - func TestReadPrivateSerializeForeignV5Key(t *testing.T) { for _, str := range foreignKeys { el, err := ReadArmoredKeyRing(strings.NewReader(str)) @@ -115,26 +30,6 @@ func TestReadPrivateSerializeForeignV5Key(t *testing.T) { } } -func TestNewEntitySerializeV5Key(t *testing.T) { - c := &packet.Config{V5Keys: true} - e, err := NewEntity("V5 Key Owner", "V5 Key", "v5@pm.me", c) - if err != nil { - t.Fatal(err) - } - checkSerializeRead(t, e) -} - -func TestNewEntityV5Key(t *testing.T) { - c := &packet.Config{ - V5Keys: true, - } - e, err := NewEntity("V5 Key Owner", "V5 Key", "v5@pm.me", c) - if err != nil { - t.Fatal(err) - } - checkV5Key(t, e) -} - func checkV5Key(t *testing.T, ent *Entity) { key := ent.PrimaryKey if key.Version != 5 { diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go new file mode 100644 index 000000000..fc9ba776d --- /dev/null +++ b/openpgp/keys_v6_test.go @@ -0,0 +1,198 @@ +package openpgp + +import ( + "bytes" + "crypto" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +var foreignKeysV6 = []string{ + v6PrivKey, + v6ArgonSealedPrivKey, +} + +func TestReadPrivateForeignV6Key(t *testing.T) { + for _, str := range foreignKeysV6 { + kring, err := ReadArmoredKeyRing(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, kring[0]) + } +} + +func TestReadPrivateForeignV6KeyAndDecrypt(t *testing.T) { + password := []byte("correct horse battery staple") + kring, err := ReadArmoredKeyRing(strings.NewReader(v6ArgonSealedPrivKey)) + if err != nil { + t.Fatal(err) + } + key := kring[0] + if key.PrivateKey != nil && key.PrivateKey.Encrypted { + if err := key.PrivateKey.Decrypt(password); err != nil { + t.Fatal(err) + } + } + for _, sub := range key.Subkeys { + if sub.PrivateKey != nil && sub.PrivateKey.Encrypted { + if err := key.PrivateKey.Decrypt(password); err != nil { + t.Fatal(err) + } + } + } + checkV6Key(t, kring[0]) +} + +func TestReadPrivateEncryptedV6Key(t *testing.T) { + c := &packet.Config{V6Keys: true} + e, err := NewEntity("V6 Key Owner", "V6 Key", "v6@pm.me", c) + if err != nil { + t.Fatal(err) + } + password := []byte("test v6 key # password") + // Encrypt private key + if err = e.PrivateKey.Encrypt(password); err != nil { + t.Fatal(err) + } + // Encrypt subkeys + for _, sub := range e.Subkeys { + if err = sub.PrivateKey.Encrypt(password); err != nil { + t.Fatal(err) + } + } + // Serialize, Read + serializedEntity := bytes.NewBuffer(nil) + err = e.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + + // Decrypt + if el[0].PrivateKey == nil { + t.Fatal("No private key found") + } + if err = el[0].PrivateKey.Decrypt(password); err != nil { + t.Error(err) + } + + // Decrypt subkeys + for _, sub := range e.Subkeys { + if err = sub.PrivateKey.Decrypt(password); err != nil { + t.Error(err) + } + } + + checkV6Key(t, el[0]) +} + +func TestNewEntitySerializeV6Key(t *testing.T) { + c := &packet.Config{V6Keys: true} + e, err := NewEntity("V6 Key Owner", "V6 Key", "v6@pm.me", c) + if err != nil { + t.Fatal(err) + } + checkSerializeReadv6(t, e) +} + +func TestNewEntityV6Key(t *testing.T) { + c := &packet.Config{ + V6Keys: true, + } + e, err := NewEntity("V6 Key Owner", "V6 Key", "v6@pm.me", c) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, e) +} + +func checkV6Key(t *testing.T, ent *Entity) { + key := ent.PrimaryKey + if key.Version != 6 { + t.Errorf("wrong key version %d", key.Version) + } + if len(key.Fingerprint) != 32 { + t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) + } + signatures := ent.Revocations + for _, id := range ent.Identities { + signatures = append(signatures, id.SelfSignature) + signatures = append(signatures, id.Signatures...) + } + for _, sig := range signatures { + if sig == nil { + continue + } + if sig.Version != 6 { + t.Errorf("wrong signature version %d", sig.Version) + } + fgptLen := len(sig.IssuerFingerprint) + if fgptLen != 32 { + t.Errorf("Wrong fingerprint length in signature: %d", fgptLen) + } + } +} + +func checkSerializeReadv6(t *testing.T, e *Entity) { + // Entity serialize + serializedEntity := bytes.NewBuffer(nil) + err := e.Serialize(serializedEntity) + if err != nil { + t.Fatal(err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, el[0]) + + // Without signing + serializedEntity = bytes.NewBuffer(nil) + err = e.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, el[0]) + + // Private + serializedEntity = bytes.NewBuffer(nil) + err = e.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, el[0]) +} + +func TestNewEntityWithDefaultHashv6(t *testing.T) { + for _, hash := range hashes[:5] { + c := &packet.Config{ + V6Keys: true, + DefaultHash: hash, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if hash == crypto.SHA1 { + if err == nil { + t.Fatal("should fail on SHA1 key creation") + } + continue + } + prefs := entity.SelfSignature.PreferredHash + if prefs == nil { + t.Fatal(err) + } + } +} diff --git a/openpgp/packet/aead_crypter.go b/openpgp/packet/aead_crypter.go index cee83bdc7..2d1aeed65 100644 --- a/openpgp/packet/aead_crypter.go +++ b/openpgp/packet/aead_crypter.go @@ -88,17 +88,20 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { if errRead != nil && errRead != io.EOF { return 0, errRead } - decrypted, errChunk := ar.openChunk(cipherChunk) - if errChunk != nil { - return 0, errChunk - } - // Return decrypted bytes, buffering if necessary - if len(dst) < len(decrypted) { - n = copy(dst, decrypted[:len(dst)]) - ar.buffer.Write(decrypted[len(dst):]) - } else { - n = copy(dst, decrypted) + if len(cipherChunk) > 0 { + decrypted, errChunk := ar.openChunk(cipherChunk) + if errChunk != nil { + return 0, errChunk + } + + // Return decrypted bytes, buffering if necessary + if len(dst) < len(decrypted) { + n = copy(dst, decrypted[:len(dst)]) + ar.buffer.Write(decrypted[len(dst):]) + } else { + n = copy(dst, decrypted) + } } // Check final authentication tag @@ -116,6 +119,12 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { // checked in the last Read call. In the future, this function could be used to // wipe the reader and peeked, decrypted bytes, if necessary. func (ar *aeadDecrypter) Close() (err error) { + if !ar.eof { + errChunk := ar.validateFinalTag(ar.peekedBytes) + if errChunk != nil { + return errChunk + } + } return nil } diff --git a/openpgp/packet/aead_encrypted_test.go b/openpgp/packet/aead_encrypted_test.go index f5827579b..97736071a 100644 --- a/openpgp/packet/aead_encrypted_test.go +++ b/openpgp/packet/aead_encrypted_test.go @@ -80,6 +80,9 @@ func TestAeadEmptyStream(t *testing.T) { } // decrypted plaintext can be read from 'rc' rc, err := packet.decrypt(key) + if err != nil { + t.Error(err) + } _, err = readDecryptedStream(rc) if err != nil { @@ -97,6 +100,9 @@ func TestAeadEmptyStream(t *testing.T) { } // decrypted plaintext can be read from 'rc' rc, err = packet.decrypt(key) + if err != nil { + t.Error(err) + } _, err = readDecryptedStream(rc) if err == nil { @@ -127,6 +133,9 @@ func TestAeadNilConfigStream(t *testing.T) { } // decrypted plaintext can be read from 'rc' rc, err := packet.decrypt(key) + if err != nil { + t.Error(err) + } got, err := readDecryptedStream(rc) if err != nil { @@ -161,6 +170,9 @@ func TestAeadStreamRandomizeSlow(t *testing.T) { } // decrypted plaintext can be read from 'rc' rc, err := packet.decrypt(key) + if err != nil { + t.Error(err) + } got, err := readDecryptedStream(rc) if err != nil { @@ -203,9 +215,13 @@ func TestAeadCorruptStreamRandomizeSlow(t *testing.T) { if err = packet.parse(contentsReader); err != nil { // Header was corrupted + t.Error(err) return } rc, err := packet.decrypt(key) + if err != nil { + t.Error(err) + } got, err := readDecryptedStream(rc) if err == nil || err == io.EOF { t.Errorf("No error raised when decrypting corrupt stream") @@ -420,7 +436,7 @@ func SerializeAEADEncrypted(w io.Writer, key []byte, config *Config) (io.WriteCl // Sample nonce nonceLen := aeadConf.Mode().IvLength() nonce := make([]byte, nonceLen) - n, err = rand.Read(nonce) + _, err = rand.Read(nonce) if err != nil { panic("Could not sample random nonce") } diff --git a/openpgp/packet/compressed.go b/openpgp/packet/compressed.go index 2f5cad71d..334de286b 100644 --- a/openpgp/packet/compressed.go +++ b/openpgp/packet/compressed.go @@ -8,9 +8,11 @@ import ( "compress/bzip2" "compress/flate" "compress/zlib" - "github.com/ProtonMail/go-crypto/openpgp/errors" "io" + "io/ioutil" "strconv" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) // Compressed represents a compressed OpenPGP packet. The decompressed contents @@ -39,6 +41,37 @@ type CompressionConfig struct { Level int } +// decompressionReader ensures that the whole compression packet is read. +type decompressionReader struct { + compressed io.Reader + decompressed io.ReadCloser + readAll bool +} + +func newDecompressionReader(r io.Reader, decompressor io.ReadCloser) *decompressionReader { + return &decompressionReader{ + compressed: r, + decompressed: decompressor, + } +} + +func (dr *decompressionReader) Read(data []byte) (n int, err error) { + if dr.readAll { + return 0, io.EOF + } + n, err = dr.decompressed.Read(data) + if err == io.EOF { + dr.readAll = true + // Close the decompressor. + if errDec := dr.decompressed.Close(); errDec != nil { + return n, errDec + } + // Consume all remaining data from the compressed packet. + consumeAll(dr.compressed) + } + return n, err +} + func (c *Compressed) parse(r io.Reader) error { var buf [1]byte _, err := readFull(r, buf[:]) @@ -50,11 +83,15 @@ func (c *Compressed) parse(r io.Reader) error { case 0: c.Body = r case 1: - c.Body = flate.NewReader(r) + c.Body = newDecompressionReader(r, flate.NewReader(r)) case 2: - c.Body, err = zlib.NewReader(r) + decompressor, err := zlib.NewReader(r) + if err != nil { + return err + } + c.Body = newDecompressionReader(r, decompressor) case 3: - c.Body = bzip2.NewReader(r) + c.Body = newDecompressionReader(r, ioutil.NopCloser(bzip2.NewReader(r))) default: err = errors.UnsupportedError("unknown compression algorithm: " + strconv.Itoa(int(buf[0]))) } diff --git a/openpgp/packet/compressed_test.go b/openpgp/packet/compressed_test.go index 9e1862160..776d20813 100644 --- a/openpgp/packet/compressed_test.go +++ b/openpgp/packet/compressed_test.go @@ -9,7 +9,6 @@ import ( "crypto/rand" "encoding/hex" "io" - "io/ioutil" mathrand "math/rand" "testing" ) @@ -31,7 +30,7 @@ func TestCompressed(t *testing.T) { return } - contents, err := ioutil.ReadAll(c.Body) + contents, err := io.ReadAll(c.Body) if err != nil && err != io.EOF { t.Error(err) return @@ -68,11 +67,14 @@ func TestCompressDecompressRandomizeFast(t *testing.T) { wcomp.Close() // Read the packet and decompress p, err := Read(w) + if err != nil { + t.Fatal(err) + } c, ok := p.(*Compressed) if !ok { t.Error("didn't find Compressed packet") } - contents, err := ioutil.ReadAll(c.Body) + contents, err := io.ReadAll(c.Body) if err != nil && err != io.EOF { t.Error(err) } diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index 04994bec9..181d5d344 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -14,6 +14,21 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/s2k" ) +var ( + defaultRejectPublicKeyAlgorithms = map[PublicKeyAlgorithm]bool{ + PubKeyAlgoElGamal: true, + PubKeyAlgoDSA: true, + } + defaultRejectMessageHashAlgorithms = map[crypto.Hash]bool{ + crypto.SHA1: true, + crypto.MD5: true, + crypto.RIPEMD160: true, + } + defaultRejectCurves = map[Curve]bool{ + CurveSecP256k1: true, + } +) + // Config collects a number of parameters along with sensible defaults. // A nil *Config is valid and results in all default values. type Config struct { @@ -73,9 +88,15 @@ type Config struct { // **Note: using this option may break compatibility with other OpenPGP // implementations, as well as future versions of this library.** AEADConfig *AEADConfig - // V5Keys configures version 5 key generation. If false, this package still - // supports version 5 keys, but produces version 4 keys. - V5Keys bool + // V6Keys configures version 6 key generation. If false, this package still + // supports version 6 keys, but produces version 4 keys. + V6Keys bool + // Minimum RSA key size allowed for key generation and message signing, verification and encryption. + MinRSABits uint16 + // Reject insecure algorithms, only works with v2 api + RejectPublicKeyAlgorithms map[PublicKeyAlgorithm]bool + RejectMessageHashAlgorithms map[crypto.Hash]bool + RejectCurves map[Curve]bool // "The validity period of the key. This is the number of seconds after // the key creation time that the key expires. If this is not present // or has a value of zero, the key never expires. This is found only on @@ -110,6 +131,21 @@ type Config struct { KnownNotations map[string]bool // SignatureNotations is a list of Notations to be added to any signatures. SignatureNotations []*Notation + // CheckIntendedRecipients controls, whether the OpenPGP Intended Recipient Fingerprint feature + // should be enabled for encryption and decryption. + // (See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#name-intended-recipient-fingerpr). + // When the flag is set, encryption produces Intended Recipient Fingerprint signature sub-packets and decryption + // checks whether the key it was encrypted to is one of the included fingerprints in the signature. + // If the flag is disabled, no Intended Recipient Fingerprint sub-packets are created or checked. + // The default behavior, when the config or flag is nil, is to enable the feature. + CheckIntendedRecipients *bool + // CacheSessionKey controls if decryption should return the session key used for decryption. + // If the flag is set, the session key is cached in the message details struct. + CacheSessionKey bool + // CheckPacketSequence is a flag that controls if the pgp message reader should strictly check + // that the packet sequence conforms with the grammar mandated by rfc4880. + // The default behavior, when the config or flag is nil, is to check the packet sequence. + CheckPacketSequence *bool } func (c *Config) Random() io.Reader { @@ -246,3 +282,71 @@ func (c *Config) Notations() []*Notation { } return c.SignatureNotations } + +func (c *Config) V6() bool { + if c == nil { + return false + } + return c.V6Keys +} + +func (c *Config) IntendedRecipients() bool { + if c == nil || c.CheckIntendedRecipients == nil { + return true + } + return *c.CheckIntendedRecipients +} + +func (c *Config) RetrieveSessionKey() bool { + if c == nil { + return false + } + return c.CacheSessionKey +} + +func (c *Config) MinimumRSABits() uint16 { + if c == nil || c.MinRSABits == 0 { + return 2047 + } + return c.MinRSABits +} + +func (c *Config) RejectPublicKeyAlgorithm(alg PublicKeyAlgorithm) bool { + var rejectedAlgorithms map[PublicKeyAlgorithm]bool + if c == nil || c.RejectPublicKeyAlgorithms == nil { + // Default + rejectedAlgorithms = defaultRejectPublicKeyAlgorithms + } else { + rejectedAlgorithms = c.RejectPublicKeyAlgorithms + } + return rejectedAlgorithms[alg] +} + +func (c *Config) RejectMessageHashAlgorithm(hash crypto.Hash) bool { + var rejectedAlgorithms map[crypto.Hash]bool + if c == nil || c.RejectMessageHashAlgorithms == nil { + // Default + rejectedAlgorithms = defaultRejectMessageHashAlgorithms + } else { + rejectedAlgorithms = c.RejectMessageHashAlgorithms + } + return rejectedAlgorithms[hash] +} + +func (c *Config) RejectCurve(curve Curve) bool { + var rejectedCurve map[Curve]bool + if c == nil || c.RejectCurves == nil { + // Default + rejectedCurve = defaultRejectCurves + } else { + rejectedCurve = c.RejectCurves + } + return rejectedCurve[curve] +} + +func (c *Config) StrictPacketSequence() bool { + if c == nil || c.CheckPacketSequence == nil { + return true + } + return *c.CheckPacketSequence +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index eeff2902c..e70f9d941 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -5,9 +5,11 @@ package packet import ( + "bytes" "crypto" "crypto/rsa" "encoding/binary" + "encoding/hex" "io" "math/big" "strconv" @@ -16,32 +18,85 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/x25519" + "github.com/ProtonMail/go-crypto/openpgp/x448" ) -const encryptedKeyVersion = 3 - // EncryptedKey represents a public-key encrypted session key. See RFC 4880, // section 5.1. type EncryptedKey struct { - KeyId uint64 - Algo PublicKeyAlgorithm - CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet - Key []byte // only valid after a successful Decrypt + Version int + KeyId uint64 + KeyVersion int // v6 + KeyFingerprint []byte // v6 + Algo PublicKeyAlgorithm + CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet + Key []byte // only valid after a successful Decrypt encryptedMPI1, encryptedMPI2 encoding.Field + ephemeralPublicX25519 *x25519.PublicKey // used for x25519 + ephemeralPublicX448 *x448.PublicKey // used for x448 + encryptedSession []byte // used for x25519 and x448 } func (e *EncryptedKey) parse(r io.Reader) (err error) { - var buf [10]byte - _, err = readFull(r, buf[:]) + var buf [8]byte + _, err = readFull(r, buf[:versionSize]) if err != nil { return } - if buf[0] != encryptedKeyVersion { + e.Version = int(buf[0]) + if e.Version != 3 && e.Version != 6 { return errors.UnsupportedError("unknown EncryptedKey version " + strconv.Itoa(int(buf[0]))) } - e.KeyId = binary.BigEndian.Uint64(buf[1:9]) - e.Algo = PublicKeyAlgorithm(buf[9]) + if e.Version == 6 { + //Read a one-octet size of the following two fields. + if _, err = readFull(r, buf[:1]); err != nil { + return + } + // The size may also be zero, and the key version and + // fingerprint omitted for an "anonymous recipient" + if buf[0] != 0 { + // non-anonymous case + _, err = readFull(r, buf[:versionSize]) + if err != nil { + return + } + e.KeyVersion = int(buf[0]) + if e.KeyVersion != 4 && e.KeyVersion != 6 { + return errors.UnsupportedError("unknown public key version " + strconv.Itoa(e.KeyVersion)) + } + var fingerprint []byte + if e.KeyVersion == 6 { + fingerprint = make([]byte, fingerprintSizeV6) + } else if e.KeyVersion == 4 { + fingerprint = make([]byte, fingerprintSize) + } + _, err = readFull(r, fingerprint) + if err != nil { + return + } + e.KeyFingerprint = fingerprint + if e.KeyVersion == 6 { + e.KeyId = binary.BigEndian.Uint64(e.KeyFingerprint[:keyIdSize]) + } else if e.KeyVersion == 4 { + e.KeyId = binary.BigEndian.Uint64(e.KeyFingerprint[fingerprintSize-keyIdSize : fingerprintSize]) + } + } + } else { + _, err = readFull(r, buf[:8]) + if err != nil { + return + } + e.KeyId = binary.BigEndian.Uint64(buf[:keyIdSize]) + } + + _, err = readFull(r, buf[:1]) + if err != nil { + return + } + e.Algo = PublicKeyAlgorithm(buf[0]) + var cipherFunction byte switch e.Algo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: e.encryptedMPI1 = new(encoding.MPI) @@ -68,26 +123,39 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { if _, err = e.encryptedMPI2.ReadFrom(r); err != nil { return } + case PubKeyAlgoX25519: + e.ephemeralPublicX25519, e.encryptedSession, cipherFunction, err = x25519.DecodeFields(r, e.Version == 6) + if err != nil { + return + } + case PubKeyAlgoX448: + e.ephemeralPublicX448, e.encryptedSession, cipherFunction, err = x448.DecodeFields(r, e.Version == 6) + if err != nil { + return + } + } + if e.Version < 6 { + switch e.Algo { + case PubKeyAlgoX25519, PubKeyAlgoX448: + e.CipherFunc = CipherFunction(cipherFunction) + // Check for validiy is in the Decrypt method + } } + _, err = consumeAll(r) return } -func checksumKeyMaterial(key []byte) uint16 { - var checksum uint16 - for _, v := range key { - checksum += uint16(v) - } - return checksum -} - // Decrypt decrypts an encrypted session key with the given private key. The // private key must have been decrypted first. // If config is nil, sensible defaults will be used. func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { - if e.KeyId != 0 && e.KeyId != priv.KeyId { + if e.Version < 6 && e.KeyId != 0 && e.KeyId != priv.KeyId { return errors.InvalidArgumentError("cannot decrypt encrypted session key for key id " + strconv.FormatUint(e.KeyId, 16) + " with private key id " + strconv.FormatUint(priv.KeyId, 16)) } + if e.Version == 6 && e.KeyVersion != 0 && !bytes.Equal(e.KeyFingerprint, priv.Fingerprint) { + return errors.InvalidArgumentError("cannot decrypt encrypted session key for key fingerprint " + hex.EncodeToString(e.KeyFingerprint) + " with private key fingerprint " + hex.EncodeToString(priv.Fingerprint)) + } if e.Algo != priv.PubKeyAlgo { return errors.InvalidArgumentError("cannot decrypt encrypted session key of type " + strconv.Itoa(int(e.Algo)) + " with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo))) } @@ -114,51 +182,110 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { m := e.encryptedMPI2.Bytes() oid := priv.PublicKey.oid.EncodedBytes() b, err = ecdh.Decrypt(priv.PrivateKey.(*ecdh.PrivateKey), vsG, m, oid, priv.PublicKey.Fingerprint[:]) + case PubKeyAlgoX25519: + b, err = x25519.Decrypt(priv.PrivateKey.(*x25519.PrivateKey), e.ephemeralPublicX25519, e.encryptedSession) + case PubKeyAlgoX448: + b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession) default: err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo))) } - if err != nil { return err } - e.CipherFunc = CipherFunction(b[0]) - if !e.CipherFunc.IsSupported() { - return errors.UnsupportedError("unsupported encryption function") - } - - e.Key = b[1 : len(b)-2] - expectedChecksum := uint16(b[len(b)-2])<<8 | uint16(b[len(b)-1]) - checksum := checksumKeyMaterial(e.Key) - if checksum != expectedChecksum { - return errors.StructuralError("EncryptedKey checksum incorrect") + var key []byte + switch priv.PubKeyAlgo { + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + keyOffset := 0 + if e.Version < 6 { + e.CipherFunc = CipherFunction(b[0]) + keyOffset = 1 + if !e.CipherFunc.IsSupported() { + return errors.UnsupportedError("unsupported encryption function") + } + } + key, err = decodeChecksumKey(b[keyOffset:]) + if err != nil { + return err + } + case PubKeyAlgoX25519, PubKeyAlgoX448: + if e.Version < 6 { + switch e.CipherFunc { + case CipherAES128, CipherAES192, CipherAES256: + break + default: + return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519 and x448") + } + } + key = b[:] + default: + return errors.UnsupportedError("unsupported algorithm for decryption") } - + e.Key = key return nil } // Serialize writes the encrypted key packet, e, to w. func (e *EncryptedKey) Serialize(w io.Writer) error { - var mpiLen int + var encodedLength int switch e.Algo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: - mpiLen = int(e.encryptedMPI1.EncodedLength()) + encodedLength = int(e.encryptedMPI1.EncodedLength()) case PubKeyAlgoElGamal: - mpiLen = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) case PubKeyAlgoECDH: - mpiLen = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + case PubKeyAlgoX25519: + encodedLength = x25519.EncodedFieldsLength(e.encryptedSession, e.Version == 6) + case PubKeyAlgoX448: + encodedLength = x448.EncodedFieldsLength(e.encryptedSession, e.Version == 6) default: return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo))) } - err := serializeHeader(w, packetTypeEncryptedKey, 1 /* version */ +8 /* key id */ +1 /* algo */ +mpiLen) + packetLen := versionSize /* version */ + keyIdSize /* key id */ + algorithmSize /* algo */ + encodedLength + if e.Version == 6 { + packetLen = versionSize /* version */ + algorithmSize /* algo */ + encodedLength + keyVersionSize /* key version */ + if e.KeyVersion == 6 { + packetLen += fingerprintSizeV6 + } else if e.KeyVersion == 4 { + packetLen += fingerprintSize + } + } + + err := serializeHeader(w, packetTypeEncryptedKey, packetLen) if err != nil { return err } - w.Write([]byte{encryptedKeyVersion}) - binary.Write(w, binary.BigEndian, e.KeyId) - w.Write([]byte{byte(e.Algo)}) + _, err = w.Write([]byte{byte(e.Version)}) + if err != nil { + return err + } + if e.Version == 6 { + _, err = w.Write([]byte{byte(e.KeyVersion)}) + if err != nil { + return err + } + // The key version number may also be zero, + // and the fingerprint omitted + if e.KeyVersion != 0 { + _, err = w.Write(e.KeyFingerprint) + if err != nil { + return err + } + } + } else { + // Write KeyID + err = binary.Write(w, binary.BigEndian, e.KeyId) + if err != nil { + return err + } + } + _, err = w.Write([]byte{byte(e.Algo)}) + if err != nil { + return err + } switch e.Algo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: @@ -176,34 +303,113 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { } _, err := w.Write(e.encryptedMPI2.EncodedBytes()) return err + case PubKeyAlgoX25519: + err := x25519.EncodeFields(w, e.ephemeralPublicX25519, e.encryptedSession, byte(e.CipherFunc), e.Version == 6) + return err + case PubKeyAlgoX448: + err := x448.EncodeFields(w, e.ephemeralPublicX448, e.encryptedSession, byte(e.CipherFunc), e.Version == 6) + return err default: panic("internal error") } } -// SerializeEncryptedKey serializes an encrypted key packet to w that contains +// SerializeEncryptedKeyAEAD serializes an encrypted key packet to w that contains // key, encrypted to pub. +// If aeadSupported is set, PKESK v6 is used else v4. // If config is nil, sensible defaults will be used. -func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, config *Config) error { - var buf [10]byte - buf[0] = encryptedKeyVersion - binary.BigEndian.PutUint64(buf[1:9], pub.KeyId) - buf[9] = byte(pub.PubKeyAlgo) - - keyBlock := make([]byte, 1 /* cipher type */ +len(key)+2 /* checksum */) - keyBlock[0] = byte(cipherFunc) - copy(keyBlock[1:], key) - checksum := checksumKeyMaterial(key) - keyBlock[1+len(key)] = byte(checksum >> 8) - keyBlock[1+len(key)+1] = byte(checksum) +func SerializeEncryptedKeyAEAD(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, config *Config) error { + return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, aeadSupported, key, false, config) +} + +// SerializeEncryptedKeyAEADwithHiddenOption serializes an encrypted key packet to w that contains +// key, encrypted to pub. +// Offers the hidden flag option to indicated if the PKESK packet should include a wildcard KeyID. +// If aeadSupported is set, PKESK v6 is used else v4. +// If config is nil, sensible defaults will be used. +func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, hidden bool, config *Config) error { + var buf [36]byte // max possible header size is v6 + lenHeaderWritten := versionSize + version := 3 + + if aeadSupported { + version = 6 + } + // An implementation MUST NOT generate ElGamal v6 PKESKs. + if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal { + return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed") + } + // In v3 PKESKs, for x25519 and x448, mandate using AES + if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) { + switch cipherFunc { + case CipherAES128, CipherAES192, CipherAES256: + break + default: + return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448") + } + } + + buf[0] = byte(version) + + // If hidden is set, the key should be hidden + // An implementation MAY accept or use a Key ID of all zeros, + // or a key version of zero and no key fingerprint, to hide the intended decryption key. + // See Section 5.1.8. in the open pgp crypto refresh + if version == 6 { + if !hidden { + // A one-octet size of the following two fields. + buf[1] = byte(keyVersionSize + len(pub.Fingerprint)) + // A one octet key version number. + buf[2] = byte(pub.Version) + lenHeaderWritten += keyVersionSize + 1 + // The fingerprint of the public key + copy(buf[lenHeaderWritten:lenHeaderWritten+len(pub.Fingerprint)], pub.Fingerprint) + lenHeaderWritten += len(pub.Fingerprint) + } else { + // The size may also be zero, and the key version + // and fingerprint omitted for an "anonymous recipient" + buf[1] = 0 + lenHeaderWritten += 1 + } + } else { + if !hidden { + binary.BigEndian.PutUint64(buf[versionSize:(versionSize+keyIdSize)], pub.KeyId) + } + lenHeaderWritten += keyIdSize + } + buf[lenHeaderWritten] = byte(pub.PubKeyAlgo) + lenHeaderWritten += algorithmSize + + var keyBlock []byte + switch pub.PubKeyAlgo { + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + lenKeyBlock := len(key) + 2 + if version < 6 { + lenKeyBlock += 1 // cipher type included + } + keyBlock = make([]byte, lenKeyBlock) + keyOffset := 0 + if version < 6 { + keyBlock[0] = byte(cipherFunc) + keyOffset = 1 + } + encodeChecksumKey(keyBlock[keyOffset:], key) + case PubKeyAlgoX25519, PubKeyAlgoX448: + // algorithm is added in plaintext below + keyBlock = key + } switch pub.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: - return serializeEncryptedKeyRSA(w, config.Random(), buf, pub.PublicKey.(*rsa.PublicKey), keyBlock) + return serializeEncryptedKeyRSA(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*rsa.PublicKey), keyBlock) case PubKeyAlgoElGamal: - return serializeEncryptedKeyElGamal(w, config.Random(), buf, pub.PublicKey.(*elgamal.PublicKey), keyBlock) + return serializeEncryptedKeyElGamal(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*elgamal.PublicKey), keyBlock) case PubKeyAlgoECDH: - return serializeEncryptedKeyECDH(w, config.Random(), buf, pub.PublicKey.(*ecdh.PublicKey), keyBlock, pub.oid, pub.Fingerprint) + return serializeEncryptedKeyECDH(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*ecdh.PublicKey), keyBlock, pub.oid, pub.Fingerprint) + case PubKeyAlgoX25519: + return serializeEncryptedKeyX25519(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x25519.PublicKey), keyBlock, byte(cipherFunc), version) + case PubKeyAlgoX448: + return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly: return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo))) } @@ -211,14 +417,30 @@ func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunctio return errors.UnsupportedError("encrypting a key to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo))) } -func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header [10]byte, pub *rsa.PublicKey, keyBlock []byte) error { +// SerializeEncryptedKey serializes an encrypted key packet to w that contains +// key, encrypted to pub. +// PKESKv6 is used if config.AEAD() is not nil. +// If config is nil, sensible defaults will be used. +func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, config *Config) error { + return SerializeEncryptedKeyAEAD(w, pub, cipherFunc, config.AEAD() != nil, key, config) +} + +// SerializeEncryptedKeyWithHiddenOption serializes an encrypted key packet to w that contains +// key, encrypted to pub. PKESKv6 is used if config.AEAD() is not nil. +// The hidden option controls if the packet should be anonymous, i.e., omit key metadata. +// If config is nil, sensible defaults will be used. +func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, hidden bool, config *Config) error { + return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) +} + +func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error { cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) if err != nil { return errors.InvalidArgumentError("RSA encryption failed: " + err.Error()) } cipherMPI := encoding.NewMPI(cipherText) - packetLen := 10 /* header length */ + int(cipherMPI.EncodedLength()) + packetLen := len(header) /* header length */ + int(cipherMPI.EncodedLength()) err = serializeHeader(w, packetTypeEncryptedKey, packetLen) if err != nil { @@ -232,13 +454,13 @@ func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header [10]byte, pub return err } -func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header [10]byte, pub *elgamal.PublicKey, keyBlock []byte) error { +func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header []byte, pub *elgamal.PublicKey, keyBlock []byte) error { c1, c2, err := elgamal.Encrypt(rand, pub, keyBlock) if err != nil { return errors.InvalidArgumentError("ElGamal encryption failed: " + err.Error()) } - packetLen := 10 /* header length */ + packetLen := len(header) /* header length */ packetLen += 2 /* mpi size */ + (c1.BitLen()+7)/8 packetLen += 2 /* mpi size */ + (c2.BitLen()+7)/8 @@ -257,7 +479,7 @@ func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header [10]byte, return err } -func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub *ecdh.PublicKey, keyBlock []byte, oid encoding.Field, fingerprint []byte) error { +func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header []byte, pub *ecdh.PublicKey, keyBlock []byte, oid encoding.Field, fingerprint []byte) error { vsG, c, err := ecdh.Encrypt(rand, pub, keyBlock, oid.EncodedBytes(), fingerprint) if err != nil { return errors.InvalidArgumentError("ECDH encryption failed: " + err.Error()) @@ -266,7 +488,7 @@ func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub g := encoding.NewMPI(vsG) m := encoding.NewOID(c) - packetLen := 10 /* header length */ + packetLen := len(header) /* header length */ packetLen += int(g.EncodedLength()) + int(m.EncodedLength()) err = serializeHeader(w, packetTypeEncryptedKey, packetLen) @@ -284,3 +506,70 @@ func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub _, err = w.Write(m.EncodedBytes()) return err } + +func serializeEncryptedKeyX25519(w io.Writer, rand io.Reader, header []byte, pub *x25519.PublicKey, keyBlock []byte, cipherFunc byte, version int) error { + ephemeralPublicX25519, ciphertext, err := x25519.Encrypt(rand, pub, keyBlock) + if err != nil { + return errors.InvalidArgumentError("x25519 encryption failed: " + err.Error()) + } + + packetLen := len(header) /* header length */ + packetLen += x25519.EncodedFieldsLength(ciphertext, version == 6) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header[:]) + if err != nil { + return err + } + return x25519.EncodeFields(w, ephemeralPublicX25519, ciphertext, cipherFunc, version == 6) +} + +func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub *x448.PublicKey, keyBlock []byte, cipherFunc byte, version int) error { + ephemeralPublicX448, ciphertext, err := x448.Encrypt(rand, pub, keyBlock) + if err != nil { + return errors.InvalidArgumentError("x448 encryption failed: " + err.Error()) + } + + packetLen := len(header) /* header length */ + packetLen += x448.EncodedFieldsLength(ciphertext, version == 6) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header[:]) + if err != nil { + return err + } + return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6) +} + +func checksumKeyMaterial(key []byte) uint16 { + var checksum uint16 + for _, v := range key { + checksum += uint16(v) + } + return checksum +} + +func decodeChecksumKey(msg []byte) (key []byte, err error) { + key = msg[:len(msg)-2] + expectedChecksum := uint16(msg[len(msg)-2])<<8 | uint16(msg[len(msg)-1]) + checksum := checksumKeyMaterial(key) + if checksum != expectedChecksum { + err = errors.StructuralError("session key checksum is incorrect") + } + return +} + +func encodeChecksumKey(buffer []byte, key []byte) { + copy(buffer, key) + checksum := checksumKeyMaterial(key) + buffer[len(key)] = byte(checksum >> 8) + buffer[len(key)+1] = byte(checksum) +} diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index fea229bd8..787c7feca 100644 --- a/openpgp/packet/encrypted_key_test.go +++ b/openpgp/packet/encrypted_key_test.go @@ -11,9 +11,13 @@ import ( "io" "math/big" "testing" + "time" "crypto" "crypto/rsa" + + "github.com/ProtonMail/go-crypto/openpgp/x25519" + "github.com/ProtonMail/go-crypto/openpgp/x448" ) func bigFromBase10(s string) *big.Int { @@ -164,7 +168,7 @@ func TestEncryptingEncryptedKey(t *testing.T) { } buf := new(bytes.Buffer) - err := SerializeEncryptedKey(buf, pub, CipherAES128, key, nil) + err := SerializeEncryptedKeyAEAD(buf, pub, CipherAES128, false, key, nil) if err != nil { t.Errorf("error writing encrypted key packet: %s", err) } @@ -202,6 +206,116 @@ func TestEncryptingEncryptedKey(t *testing.T) { } } +func TestEncryptingEncryptedKeyV6(t *testing.T) { + key := []byte{1, 2, 3, 4} + config := &Config{ + AEADConfig: &AEADConfig{}, + } + rsaKey, _ := rsa.GenerateKey(config.Random(), 2048) + rsaWrappedKey := NewRSAPrivateKey(time.Now(), rsaKey) + rsaWrappedKey.UpgradeToV6() + rsaWrappedKeyPub := &rsaWrappedKey.PublicKey + + buf := new(bytes.Buffer) + err := SerializeEncryptedKeyAEAD(buf, rsaWrappedKeyPub, CipherAES128, true, key, config) + + if err != nil { + t.Errorf("error writing encrypted key packet: %s", err) + } + + p, err := Read(buf) + if err != nil { + t.Errorf("error from Read: %s", err) + return + } + ek, ok := p.(*EncryptedKey) + if !ok { + t.Errorf("didn't parse an EncryptedKey, got %#v", p) + return + } + + if !bytes.Equal(ek.KeyFingerprint, rsaWrappedKey.Fingerprint) || + ek.Algo != PubKeyAlgoRSA || + ek.KeyVersion != rsaWrappedKey.Version { + t.Errorf("unexpected EncryptedKey contents: %#v", ek) + return + } + + err = ek.Decrypt(rsaWrappedKey, nil) + if err != nil { + t.Errorf("error from Decrypt: %s", err) + return + } + + keyHex := fmt.Sprintf("%x", ek.Key) + expectedKeyHex := fmt.Sprintf("%x", key) + if keyHex != expectedKeyHex { + t.Errorf("bad key, got %s want %s", keyHex, expectedKeyHex) + } +} + +func TestEncryptingEncryptedKeyXAlgorithms(t *testing.T) { + key := []byte{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4} + config := &Config{ + AEADConfig: &AEADConfig{}, + } + x25519Gen := func() (*PrivateKey, PublicKeyAlgorithm) { + x25519Key, _ := x25519.GenerateKey(config.Random()) + x25519WrappedKey := NewX25519PrivateKey(time.Now(), x25519Key) + x25519WrappedKey.UpgradeToV6() + return x25519WrappedKey, PubKeyAlgoX25519 + } + x448Gen := func() (*PrivateKey, PublicKeyAlgorithm) { + x448Key, _ := x448.GenerateKey(config.Random()) + x448WrappedKey := NewX448PrivateKey(time.Now(), x448Key) + x448WrappedKey.UpgradeToV6() + return x448WrappedKey, PubKeyAlgoX448 + } + testCaseFunc := []func() (*PrivateKey, PublicKeyAlgorithm){x25519Gen, x448Gen} + + for _, genFunc := range testCaseFunc { + wrappedKey, pubType := genFunc() + wrappedKeyPub := &wrappedKey.PublicKey + + buf := new(bytes.Buffer) + err := SerializeEncryptedKeyAEAD(buf, wrappedKeyPub, CipherAES128, true, key, config) + + if err != nil { + t.Errorf("error writing encrypted key packet: %s", err) + } + + p, err := Read(buf) + if err != nil { + t.Errorf("error from Read: %s", err) + return + } + ek, ok := p.(*EncryptedKey) + if !ok { + t.Errorf("didn't parse an EncryptedKey, got %#v", p) + return + } + + if !bytes.Equal(ek.KeyFingerprint, wrappedKey.Fingerprint) || + ek.Algo != pubType || + ek.KeyVersion != wrappedKey.Version { + t.Errorf("unexpected EncryptedKey contents: %#v", ek) + return + } + + err = ek.Decrypt(wrappedKey, nil) + if err != nil { + t.Errorf("error from Decrypt: %s", err) + return + } + + keyHex := fmt.Sprintf("%x", ek.Key) + expectedKeyHex := fmt.Sprintf("%x", key) + if keyHex != expectedKeyHex { + t.Errorf("bad key, got %s want %s", keyHex, expectedKeyHex) + } + } +} + func TestSerializingEncryptedKey(t *testing.T) { const encryptedKeyHex = "c18c032a67d68660df41c70104005789d0de26b6a50c985a02a13131ca829c413a35d0e6fa8d6842599252162808ac7439c72151c8c6183e76923fe3299301414d0c25a2f06a2257db3839e7df0ec964773f6e4c4ac7ff3b48c444237166dd46ba8ff443a5410dc670cb486672fdbe7c9dfafb75b4fea83af3a204fe2a7dfa86bd20122b4f3d2646cbeecb8f7be8" diff --git a/openpgp/packet/literal.go b/openpgp/packet/literal.go index 4be987609..8a028c8a1 100644 --- a/openpgp/packet/literal.go +++ b/openpgp/packet/literal.go @@ -58,9 +58,9 @@ func (l *LiteralData) parse(r io.Reader) (err error) { // on completion. The fileName is truncated to 255 bytes. func SerializeLiteral(w io.WriteCloser, isBinary bool, fileName string, time uint32) (plaintext io.WriteCloser, err error) { var buf [4]byte - buf[0] = 't' - if isBinary { - buf[0] = 'b' + buf[0] = 'b' + if !isBinary { + buf[0] = 'u' } if len(fileName) > 255 { fileName = fileName[:255] diff --git a/openpgp/packet/marker.go b/openpgp/packet/marker.go new file mode 100644 index 000000000..1ee378ba3 --- /dev/null +++ b/openpgp/packet/marker.go @@ -0,0 +1,33 @@ +package packet + +import ( + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +type Marker struct{} + +const markerString = "PGP" + +// parse just checks if the packet contains "PGP". +func (m *Marker) parse(reader io.Reader) error { + var buffer [3]byte + if _, err := io.ReadFull(reader, buffer[:]); err != nil { + return err + } + if string(buffer[:]) != markerString { + return errors.StructuralError("invalid marker packet") + } + return nil +} + +// SerializeMarker writes a marker packet to writer. +func SerializeMarker(writer io.Writer) error { + err := serializeHeader(writer, packetTypeMarker, len(markerString)) + if err != nil { + return err + } + _, err = writer.Write([]byte(markerString)) + return err +} diff --git a/openpgp/packet/one_pass_signature.go b/openpgp/packet/one_pass_signature.go index 033fb2d7e..f393c4063 100644 --- a/openpgp/packet/one_pass_signature.go +++ b/openpgp/packet/one_pass_signature.go @@ -7,34 +7,37 @@ package packet import ( "crypto" "encoding/binary" - "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "io" "strconv" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" ) // OnePassSignature represents a one-pass signature packet. See RFC 4880, // section 5.4. type OnePassSignature struct { - SigType SignatureType - Hash crypto.Hash - PubKeyAlgo PublicKeyAlgorithm - KeyId uint64 - IsLast bool + Version int + SigType SignatureType + Hash crypto.Hash + PubKeyAlgo PublicKeyAlgorithm + KeyId uint64 + IsLast bool + Salt []byte // v6 only + KeyFingerprint []byte // v6 only } -const onePassSignatureVersion = 3 - func (ops *OnePassSignature) parse(r io.Reader) (err error) { - var buf [13]byte - - _, err = readFull(r, buf[:]) + var buf [8]byte + // Read: version | signature type | hash algorithm | public-key algorithm + _, err = readFull(r, buf[:4]) if err != nil { return } - if buf[0] != onePassSignatureVersion { - err = errors.UnsupportedError("one-pass-signature packet version " + strconv.Itoa(int(buf[0]))) + if buf[0] != 3 && buf[0] != 6 { + return errors.UnsupportedError("one-pass-signature packet version " + strconv.Itoa(int(buf[0]))) } + ops.Version = int(buf[0]) var ok bool ops.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2]) @@ -44,15 +47,69 @@ func (ops *OnePassSignature) parse(r io.Reader) (err error) { ops.SigType = SignatureType(buf[1]) ops.PubKeyAlgo = PublicKeyAlgorithm(buf[3]) - ops.KeyId = binary.BigEndian.Uint64(buf[4:12]) - ops.IsLast = buf[12] != 0 + + if ops.Version == 6 { + // Only for v6, a variable-length field containing the salt + _, err = readFull(r, buf[:1]) + if err != nil { + return + } + saltLength := int(buf[0]) + var expectedSaltLength int + expectedSaltLength, err = SaltLengthForHash(ops.Hash) + if err != nil { + return + } + if saltLength != expectedSaltLength { + err = errors.StructuralError("unexpected salt size for the given hash algorithm") + return + } + salt := make([]byte, expectedSaltLength) + _, err = readFull(r, salt) + if err != nil { + return + } + ops.Salt = salt + + // Only for v6 packets, 32 octets of the fingerprint of the signing key. + fingerprint := make([]byte, 32) + _, err = readFull(r, fingerprint) + if err != nil { + return + } + ops.KeyFingerprint = fingerprint + ops.KeyId = binary.BigEndian.Uint64(ops.KeyFingerprint[:8]) + } else { + _, err = readFull(r, buf[:8]) + if err != nil { + return + } + ops.KeyId = binary.BigEndian.Uint64(buf[:8]) + } + + _, err = readFull(r, buf[:1]) + if err != nil { + return + } + ops.IsLast = buf[0] != 0 return } // Serialize marshals the given OnePassSignature to w. func (ops *OnePassSignature) Serialize(w io.Writer) error { - var buf [13]byte - buf[0] = onePassSignatureVersion + //v3 length 1+1+1+1+8+1 = + packetLength := 13 + if ops.Version == 6 { + // v6 length 1+1+1+1+1+len(salt)+32+1 = + packetLength = 38 + len(ops.Salt) + } + + if err := serializeHeader(w, packetTypeOnePassSignature, packetLength); err != nil { + return err + } + + var buf [8]byte + buf[0] = byte(ops.Version) buf[1] = uint8(ops.SigType) var ok bool buf[2], ok = algorithm.HashToHashIdWithSha1(ops.Hash) @@ -60,14 +117,41 @@ func (ops *OnePassSignature) Serialize(w io.Writer) error { return errors.UnsupportedError("hash type: " + strconv.Itoa(int(ops.Hash))) } buf[3] = uint8(ops.PubKeyAlgo) - binary.BigEndian.PutUint64(buf[4:12], ops.KeyId) - if ops.IsLast { - buf[12] = 1 - } - if err := serializeHeader(w, packetTypeOnePassSignature, len(buf)); err != nil { + _, err := w.Write(buf[:4]) + if err != nil { return err } - _, err := w.Write(buf[:]) + + if ops.Version == 6 { + // write salt for v6 signatures + _, err := w.Write([]byte{uint8(len(ops.Salt))}) + if err != nil { + return err + } + _, err = w.Write(ops.Salt) + if err != nil { + return err + } + + // write fingerprint v6 signatures + _, err = w.Write(ops.KeyFingerprint) + if err != nil { + return err + } + } else { + binary.BigEndian.PutUint64(buf[:8], ops.KeyId) + _, err := w.Write(buf[:8]) + if err != nil { + return err + } + } + + isLast := []byte{byte(0)} + if ops.IsLast { + isLast[0] = 1 + } + + _, err = w.Write(isLast) return err } diff --git a/openpgp/packet/opaque.go b/openpgp/packet/opaque.go index 4f8204079..cef7c661d 100644 --- a/openpgp/packet/opaque.go +++ b/openpgp/packet/opaque.go @@ -7,7 +7,6 @@ package packet import ( "bytes" "io" - "io/ioutil" "github.com/ProtonMail/go-crypto/openpgp/errors" ) @@ -26,7 +25,7 @@ type OpaquePacket struct { } func (op *OpaquePacket) parse(r io.Reader) (err error) { - op.Contents, err = ioutil.ReadAll(r) + op.Contents, err = io.ReadAll(r) return } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 4d86a7da8..da12fbce0 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -4,7 +4,7 @@ // Package packet implements parsing and serialization of OpenPGP packets, as // specified in RFC 4880. -package packet // import "github.com/ProtonMail/go-crypto/openpgp/packet" +package packet // import "github.com/ProtonMail/go-crypto/v2/openpgp/packet" import ( "bytes" @@ -311,12 +311,15 @@ const ( packetTypePrivateSubkey packetType = 7 packetTypeCompressed packetType = 8 packetTypeSymmetricallyEncrypted packetType = 9 + packetTypeMarker packetType = 10 packetTypeLiteralData packetType = 11 + packetTypeTrust packetType = 12 packetTypeUserId packetType = 13 packetTypePublicSubkey packetType = 14 packetTypeUserAttribute packetType = 17 packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18 packetTypeAEADEncrypted packetType = 20 + packetPadding packetType = 21 ) // EncryptedDataPacket holds encrypted data. It is currently implemented by @@ -328,7 +331,7 @@ type EncryptedDataPacket interface { // Read reads a single OpenPGP packet from the given io.Reader. If there is an // error parsing a packet, the whole packet is consumed from the input. func Read(r io.Reader) (p Packet, err error) { - tag, _, contents, err := readHeader(r) + tag, len, contents, err := readHeader(r) if err != nil { return } @@ -367,8 +370,93 @@ func Read(r io.Reader) (p Packet, err error) { p = se case packetTypeAEADEncrypted: p = new(AEADEncrypted) + case packetPadding: + p = Padding(len) + case packetTypeMarker: + p = new(Marker) + case packetTypeTrust: + // Not implemented, just consume + err = errors.UnknownPacketTypeError(tag) default: + // Packet Tags from 0 to 39 are critical. + // Packet Tags from 40 to 63 are non-critical. + if tag < 40 { + err = errors.CriticalUnknownPacketTypeError(tag) + } else { + err = errors.UnknownPacketTypeError(tag) + } + } + if p != nil { + err = p.parse(contents) + } + if err != nil { + consumeAll(contents) + } + return +} + +// ReadWithCheck reads a single OpenPGP message packet from the given io.Reader. If there is an +// error parsing a packet, the whole packet is consumed from the input. +// ReadWithCheck additionally checks if the OpenPGP message packet sequence adheres +// to the packet composition rules in rfc4880, if not throws an error. +func ReadWithCheck(r io.Reader, sequence *SequenceVerifier) (p Packet, msgErr error, err error) { + tag, len, contents, err := readHeader(r) + if err != nil { + return + } + switch tag { + case packetTypeEncryptedKey: + msgErr = sequence.Next(ESKSymbol) + p = new(EncryptedKey) + case packetTypeSignature: + msgErr = sequence.Next(SigSymbol) + p = new(Signature) + case packetTypeSymmetricKeyEncrypted: + msgErr = sequence.Next(ESKSymbol) + p = new(SymmetricKeyEncrypted) + case packetTypeOnePassSignature: + msgErr = sequence.Next(OPSSymbol) + p = new(OnePassSignature) + case packetTypeCompressed: + msgErr = sequence.Next(CompSymbol) + p = new(Compressed) + case packetTypeSymmetricallyEncrypted: + msgErr = sequence.Next(EncSymbol) + p = new(SymmetricallyEncrypted) + case packetTypeLiteralData: + msgErr = sequence.Next(LDSymbol) + p = new(LiteralData) + case packetTypeSymmetricallyEncryptedIntegrityProtected: + msgErr = sequence.Next(EncSymbol) + se := new(SymmetricallyEncrypted) + se.IntegrityProtected = true + p = se + case packetTypeAEADEncrypted: + msgErr = sequence.Next(EncSymbol) + p = new(AEADEncrypted) + case packetPadding: + p = Padding(len) + case packetTypeMarker: + p = new(Marker) + case packetTypeTrust: + // Not implemented, just consume err = errors.UnknownPacketTypeError(tag) + case packetTypePrivateKey, + packetTypePrivateSubkey, + packetTypePublicKey, + packetTypePublicSubkey, + packetTypeUserId, + packetTypeUserAttribute: + msgErr = sequence.Next(UnknownSymbol) + consumeAll(contents) + default: + // Packet Tags from 0 to 39 are critical. + // Packet Tags from 40 to 63 are non-critical. + if tag < 40 { + err = errors.CriticalUnknownPacketTypeError(tag) + } else { + err = errors.UnknownPacketTypeError(tag) + } } if p != nil { err = p.parse(contents) @@ -385,17 +473,17 @@ type SignatureType uint8 const ( SigTypeBinary SignatureType = 0x00 - SigTypeText = 0x01 - SigTypeGenericCert = 0x10 - SigTypePersonaCert = 0x11 - SigTypeCasualCert = 0x12 - SigTypePositiveCert = 0x13 - SigTypeSubkeyBinding = 0x18 - SigTypePrimaryKeyBinding = 0x19 - SigTypeDirectSignature = 0x1F - SigTypeKeyRevocation = 0x20 - SigTypeSubkeyRevocation = 0x28 - SigTypeCertificationRevocation = 0x30 + SigTypeText SignatureType = 0x01 + SigTypeGenericCert SignatureType = 0x10 + SigTypePersonaCert SignatureType = 0x11 + SigTypeCasualCert SignatureType = 0x12 + SigTypePositiveCert SignatureType = 0x13 + SigTypeSubkeyBinding SignatureType = 0x18 + SigTypePrimaryKeyBinding SignatureType = 0x19 + SigTypeDirectSignature SignatureType = 0x1F + SigTypeKeyRevocation SignatureType = 0x20 + SigTypeSubkeyRevocation SignatureType = 0x28 + SigTypeCertificationRevocation SignatureType = 0x30 ) // PublicKeyAlgorithm represents the different public key system specified for @@ -412,6 +500,11 @@ const ( PubKeyAlgoECDSA PublicKeyAlgorithm = 19 // https://www.ietf.org/archive/id/draft-koch-eddsa-for-openpgp-04.txt PubKeyAlgoEdDSA PublicKeyAlgorithm = 22 + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh + PubKeyAlgoX25519 PublicKeyAlgorithm = 25 + PubKeyAlgoX448 PublicKeyAlgorithm = 26 + PubKeyAlgoEd25519 PublicKeyAlgorithm = 27 + PubKeyAlgoEd448 PublicKeyAlgorithm = 28 // Deprecated in RFC 4880, Section 13.5. Use key flags instead. PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2 @@ -422,7 +515,7 @@ const ( // key of the given type. func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448: return true } return false @@ -432,7 +525,7 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { // sign a message. func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: return true } return false @@ -512,6 +605,11 @@ func (mode AEADMode) TagLength() int { return algorithm.AEADMode(mode).TagLength() } +// IsSupported returns true if the aead mode is supported from the library +func (mode AEADMode) IsSupported() bool { + return algorithm.AEADMode(mode).TagLength() > 0 +} + // new returns a fresh instance of the given mode. func (mode AEADMode) new(block cipher.Block) cipher.AEAD { return algorithm.AEADMode(mode).New(block) @@ -526,8 +624,17 @@ const ( KeySuperseded ReasonForRevocation = 1 KeyCompromised ReasonForRevocation = 2 KeyRetired ReasonForRevocation = 3 + UserIDNotValid ReasonForRevocation = 32 + Unknown ReasonForRevocation = 200 ) +func NewReasonForRevocation(value byte) ReasonForRevocation { + if value < 4 || value == 32 { + return ReasonForRevocation(value) + } + return Unknown +} + // Curve is a mapping to supported ECC curves for key generation. // See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#name-curve-specific-wire-formats type Curve string @@ -549,3 +656,20 @@ type TrustLevel uint8 // TrustAmount represents a trust amount per RFC4880 5.2.3.13 type TrustAmount uint8 + +const ( + // versionSize is the length in bytes of the version value. + versionSize = 1 + // algorithmSize is the length in bytes of the key algorithm value. + algorithmSize = 1 + // keyVersionSize is the length in bytes of the key version value + keyVersionSize = 1 + // keyIdSize is the length in bytes of the key identifier value. + keyIdSize = 8 + // timestampSize is the length in bytes of encoded timestamps. + timestampSize = 4 + // fingerprintSizeV6 is the length in bytes of the key fingerprint in v6. + fingerprintSizeV6 = 32 + // fingerprintSize is the length in bytes of the key fingerprint. + fingerprintSize = 20 +) diff --git a/openpgp/packet/packet_sequence.go b/openpgp/packet/packet_sequence.go new file mode 100644 index 000000000..55a8a56c2 --- /dev/null +++ b/openpgp/packet/packet_sequence.go @@ -0,0 +1,222 @@ +package packet + +// This file implements the pushdown automata (PDA) from PGPainless (Paul Schaub) +// to verify pgp packet sequences. See Paul's blogpost for more details: +// https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/ +import ( + "fmt" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +func NewErrMalformedMessage(from State, input InputSymbol, stackSymbol StackSymbol) errors.ErrMalformedMessage { + return errors.ErrMalformedMessage(fmt.Sprintf("state %d, input symbol %d, stack symbol %d ", from, input, stackSymbol)) +} + +// InputSymbol defines the input alphabet of the PDA +type InputSymbol uint8 + +const ( + LDSymbol InputSymbol = iota + SigSymbol + OPSSymbol + CompSymbol + ESKSymbol + EncSymbol + EOSSymbol + UnknownSymbol +) + +// StackSymbol defines the stack alphabet of the PDA +type StackSymbol int8 + +const ( + MsgStackSymbol StackSymbol = iota + OpsStackSymbol + KeyStackSymbol + EndStackSymbol + EmptyStackSymbol +) + +// State defines the states of the PDA +type State int8 + +const ( + OpenPGPMessage State = iota + ESKMessage + LiteralMessage + CompressedMessage + EncryptedMessage + ValidMessage +) + +// transition represents a state transition in the PDA +type transition func(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) + +// SequenceVerifier is a pushdown automata to verify +// PGP messages packet sequences according to rfc4880. +type SequenceVerifier struct { + stack []StackSymbol + state State +} + +// Next performs a state transition with the given input symbol. +// If the transition fails a ErrMalformedMessage is returned. +func (sv *SequenceVerifier) Next(input InputSymbol) error { + for { + stackSymbol := sv.popStack() + transitionFunc := getTransition(sv.state) + nextState, newStackSymbols, redo, err := transitionFunc(input, stackSymbol) + if err != nil { + return err + } + if redo { + sv.pushStack(stackSymbol) + } + for _, newStackSymbol := range newStackSymbols { + sv.pushStack(newStackSymbol) + } + sv.state = nextState + if !redo { + break + } + } + return nil +} + +// Valid returns true if RDA is in a valid state. +func (sv *SequenceVerifier) Valid() bool { + return sv.state == ValidMessage && len(sv.stack) == 0 +} + +func (sv *SequenceVerifier) AssertValid() error { + if !sv.Valid() { + return errors.ErrMalformedMessage("invalid message") + } + return nil +} + +func NewSequenceVerifier() *SequenceVerifier { + return &SequenceVerifier{ + stack: []StackSymbol{EndStackSymbol, MsgStackSymbol}, + state: OpenPGPMessage, + } +} + +func (sv *SequenceVerifier) popStack() StackSymbol { + if len(sv.stack) == 0 { + return EmptyStackSymbol + } + elemIndex := len(sv.stack) - 1 + stackSymbol := sv.stack[elemIndex] + sv.stack = sv.stack[:elemIndex] + return stackSymbol +} + +func (sv *SequenceVerifier) pushStack(stackSymbol StackSymbol) { + sv.stack = append(sv.stack, stackSymbol) +} + +func getTransition(from State) transition { + switch from { + case OpenPGPMessage: + return fromOpenPGPMessage + case LiteralMessage: + return fromLiteralMessage + case CompressedMessage: + return fromCompressedMessage + case EncryptedMessage: + return fromEncryptedMessage + case ESKMessage: + return fromESKMessage + case ValidMessage: + return fromValidMessage + } + return nil +} + +// fromOpenPGPMessage is the transition for the state OpenPGPMessage. +func fromOpenPGPMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) { + if stackSymbol != MsgStackSymbol { + return 0, nil, false, NewErrMalformedMessage(OpenPGPMessage, input, stackSymbol) + } + switch input { + case LDSymbol: + return LiteralMessage, nil, false, nil + case SigSymbol: + return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, false, nil + case OPSSymbol: + return OpenPGPMessage, []StackSymbol{OpsStackSymbol, MsgStackSymbol}, false, nil + case CompSymbol: + return CompressedMessage, nil, false, nil + case ESKSymbol: + return ESKMessage, []StackSymbol{KeyStackSymbol}, false, nil + case EncSymbol: + return EncryptedMessage, nil, false, nil + } + return 0, nil, false, NewErrMalformedMessage(OpenPGPMessage, input, stackSymbol) +} + +// fromESKMessage is the transition for the state ESKMessage. +func fromESKMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) { + if stackSymbol != KeyStackSymbol { + return 0, nil, false, NewErrMalformedMessage(ESKMessage, input, stackSymbol) + } + switch input { + case ESKSymbol: + return ESKMessage, []StackSymbol{KeyStackSymbol}, false, nil + case EncSymbol: + return EncryptedMessage, nil, false, nil + } + return 0, nil, false, NewErrMalformedMessage(ESKMessage, input, stackSymbol) +} + +// fromLiteralMessage is the transition for the state LiteralMessage. +func fromLiteralMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) { + switch input { + case SigSymbol: + if stackSymbol == OpsStackSymbol { + return LiteralMessage, nil, false, nil + } + case EOSSymbol: + if stackSymbol == EndStackSymbol { + return ValidMessage, nil, false, nil + } + } + return 0, nil, false, NewErrMalformedMessage(LiteralMessage, input, stackSymbol) +} + +// fromLiteralMessage is the transition for the state CompressedMessage. +func fromCompressedMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) { + switch input { + case SigSymbol: + if stackSymbol == OpsStackSymbol { + return CompressedMessage, nil, false, nil + } + case EOSSymbol: + if stackSymbol == EndStackSymbol { + return ValidMessage, nil, false, nil + } + } + return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, true, nil +} + +// fromEncryptedMessage is the transition for the state EncryptedMessage. +func fromEncryptedMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) { + switch input { + case SigSymbol: + if stackSymbol == OpsStackSymbol { + return EncryptedMessage, nil, false, nil + } + case EOSSymbol: + if stackSymbol == EndStackSymbol { + return ValidMessage, nil, false, nil + } + } + return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, true, nil +} + +// fromValidMessage is the transition for the state ValidMessage. +func fromValidMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) { + return 0, nil, false, NewErrMalformedMessage(ValidMessage, input, stackSymbol) +} diff --git a/openpgp/packet/packet_test.go b/openpgp/packet/packet_test.go index 770b8b5ba..526871df0 100644 --- a/openpgp/packet/packet_test.go +++ b/openpgp/packet/packet_test.go @@ -9,7 +9,6 @@ import ( "encoding/hex" "fmt" "io" - "io/ioutil" "testing" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -101,7 +100,7 @@ var partialLengthReaderTests = []struct { func TestPartialLengthReader(t *testing.T) { for i, test := range partialLengthReaderTests { r := &partialLengthReader{readerFromHex(test.hexInput), 0, true} - out, err := ioutil.ReadAll(r) + out, err := io.ReadAll(r) if test.err != nil { if err != test.err { t.Errorf("%d: expected different error got:%s want:%s", i, err, test.err) @@ -173,7 +172,7 @@ func TestReadHeader(t *testing.T) { continue } - body, err := ioutil.ReadAll(contents) + body, err := io.ReadAll(contents) if err != nil { if !test.unexpectedEOF || err != io.ErrUnexpectedEOF { t.Errorf("%d: unexpected error from contents: %s", i, err) diff --git a/openpgp/packet/packet_unsupported.go b/openpgp/packet/packet_unsupported.go new file mode 100644 index 000000000..2d714723c --- /dev/null +++ b/openpgp/packet/packet_unsupported.go @@ -0,0 +1,24 @@ +package packet + +import ( + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +// UnsupportedPackage represents a OpenPGP packet with a known packet type +// but with unsupported content. +type UnsupportedPacket struct { + IncompletePacket Packet + Error errors.UnsupportedError +} + +// Implements the Packet interface +func (up *UnsupportedPacket) parse(read io.Reader) error { + err := up.IncompletePacket.parse(read) + if castedErr, ok := err.(errors.UnsupportedError); ok { + up.Error = castedErr + return nil + } + return err +} diff --git a/openpgp/packet/padding.go b/openpgp/packet/padding.go new file mode 100644 index 000000000..06fa83740 --- /dev/null +++ b/openpgp/packet/padding.go @@ -0,0 +1,27 @@ +package packet + +import ( + "io" + "io/ioutil" +) + +// Padding type represents a Padding Packet (Tag 21). +// The padding type is represented by the length of its padding. +// see https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-padding-packet-tag-21 +type Padding int + +// parse just ignores the padding content. +func (pad Padding) parse(reader io.Reader) error { + _, err := io.CopyN(ioutil.Discard, reader, int64(pad)) + return err +} + +// SerializePadding writes the padding to writer. +func (pad Padding) SerializePadding(writer io.Writer, rand io.Reader) error { + err := serializeHeader(writer, packetPadding, int(pad)) + if err != nil { + return err + } + _, err = io.CopyN(writer, rand, int64(pad)) + return err +} diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 2fc438643..099b4d9ba 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -9,22 +9,28 @@ import ( "crypto" "crypto/cipher" "crypto/dsa" - "crypto/rand" "crypto/rsa" "crypto/sha1" + "crypto/sha256" + "crypto/subtle" + "fmt" "io" - "io/ioutil" "math/big" "strconv" "time" "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/ed25519" + "github.com/ProtonMail/go-crypto/openpgp/ed448" "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/x25519" + "github.com/ProtonMail/go-crypto/openpgp/x448" + "golang.org/x/crypto/hkdf" ) // PrivateKey represents a possibly encrypted private key. See RFC 4880, @@ -35,14 +41,14 @@ type PrivateKey struct { encryptedData []byte cipher CipherFunction s2k func(out, in []byte) - // An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519}.PrivateKey or + aead AEADMode // only relevant if S2KAEAD is enabled + // An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519|ed448}.PrivateKey or // crypto.Signer/crypto.Decrypter (Decryptor RSA only). - PrivateKey interface{} - sha1Checksum bool - iv []byte + PrivateKey interface{} + iv []byte // Type of encryption of the S2K packet - // Allowed values are 0 (Not encrypted), 254 (SHA1), or + // Allowed values are 0 (Not encrypted), 253 (AEAD), 254 (SHA1), or // 255 (2-byte checksum) s2kType S2KType // Full parameters of the S2K packet @@ -55,6 +61,8 @@ type S2KType uint8 const ( // S2KNON unencrypt S2KNON S2KType = 0 + // S2KAEAD use authenticated encryption + S2KAEAD S2KType = 253 // S2KSHA1 sha1 sum check S2KSHA1 S2KType = 254 // S2KCHECKSUM sum check @@ -103,6 +111,34 @@ func NewECDHPrivateKey(creationTime time.Time, priv *ecdh.PrivateKey) *PrivateKe return pk } +func NewX25519PrivateKey(creationTime time.Time, priv *x25519.PrivateKey) *PrivateKey { + pk := new(PrivateKey) + pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) + pk.PrivateKey = priv + return pk +} + +func NewX448PrivateKey(creationTime time.Time, priv *x448.PrivateKey) *PrivateKey { + pk := new(PrivateKey) + pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) + pk.PrivateKey = priv + return pk +} + +func NewEd25519PrivateKey(creationTime time.Time, priv *ed25519.PrivateKey) *PrivateKey { + pk := new(PrivateKey) + pk.PublicKey = *NewEd25519PublicKey(creationTime, &priv.PublicKey) + pk.PrivateKey = priv + return pk +} + +func NewEd448PrivateKey(creationTime time.Time, priv *ed448.PrivateKey) *PrivateKey { + pk := new(PrivateKey) + pk.PublicKey = *NewEd448PublicKey(creationTime, &priv.PublicKey) + pk.PrivateKey = priv + return pk +} + // NewSignerPrivateKey creates a PrivateKey from a crypto.Signer that // implements RSA, ECDSA or EdDSA. func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey { @@ -122,6 +158,14 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey) case eddsa.PrivateKey: pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey) + case *ed25519.PrivateKey: + pk.PublicKey = *NewEd25519PublicKey(creationTime, &pubkey.PublicKey) + case ed25519.PrivateKey: + pk.PublicKey = *NewEd25519PublicKey(creationTime, &pubkey.PublicKey) + case *ed448.PrivateKey: + pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) + case ed448.PrivateKey: + pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -129,7 +173,7 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey return pk } -// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh}.PrivateKey. +// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448}.PrivateKey. func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey { pk := new(PrivateKey) switch priv := decrypter.(type) { @@ -139,6 +183,10 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewElGamalPublicKey(creationTime, &priv.PublicKey) case *ecdh.PrivateKey: pk.PublicKey = *NewECDHPublicKey(creationTime, &priv.PublicKey) + case *x25519.PrivateKey: + pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) + case *x448.PrivateKey: + pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } @@ -152,6 +200,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { return } v5 := pk.PublicKey.Version == 5 + v6 := pk.PublicKey.Version == 6 var buf [1]byte _, err = readFull(r, buf[:]) @@ -160,7 +209,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { } pk.s2kType = S2KType(buf[0]) var optCount [1]byte - if v5 { + if v5 || (v6 && pk.s2kType != S2KNON) { if _, err = readFull(r, optCount[:]); err != nil { return } @@ -170,9 +219,9 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { case S2KNON: pk.s2k = nil pk.Encrypted = false - case S2KSHA1, S2KCHECKSUM: - if v5 && pk.s2kType == S2KCHECKSUM { - return errors.StructuralError("wrong s2k identifier for version 5") + case S2KSHA1, S2KCHECKSUM, S2KAEAD: + if (v5 || v6) && pk.s2kType == S2KCHECKSUM { + return errors.StructuralError(fmt.Sprintf("wrong s2k identifier for version %d", pk.Version)) } _, err = readFull(r, buf[:]) if err != nil { @@ -182,6 +231,29 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { if pk.cipher != 0 && !pk.cipher.IsSupported() { return errors.UnsupportedError("unsupported cipher function in private key") } + // [Optional] If string-to-key usage octet was 253, + // a one-octet AEAD algorithm. + if pk.s2kType == S2KAEAD { + _, err = readFull(r, buf[:]) + if err != nil { + return + } + pk.aead = AEADMode(buf[0]) + if !pk.aead.IsSupported() { + return errors.UnsupportedError("unsupported aead mode in private key") + } + } + + // [Optional] Only for a version 6 packet, + // and if string-to-key usage octet was 255, 254, or 253, + // an one-octet count of the following field. + if v6 { + _, err = readFull(r, buf[:]) + if err != nil { + return + } + } + pk.s2kParams, err = s2k.ParseIntoParams(r) if err != nil { return @@ -194,23 +266,32 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { return } pk.Encrypted = true - if pk.s2kType == S2KSHA1 { - pk.sha1Checksum = true - } default: return errors.UnsupportedError("deprecated s2k function in private key") } if pk.Encrypted { - blockSize := pk.cipher.blockSize() - if blockSize == 0 { + var ivSize int + // If the S2K usage octet was 253, the IV is of the size expected by the AEAD mode, + // unless it's a version 5 key, in which case it's the size of the symmetric cipher's block size. + // For all other S2K modes, it's always the block size. + if !v5 && pk.s2kType == S2KAEAD { + ivSize = pk.aead.IvLength() + } else { + ivSize = pk.cipher.blockSize() + } + + if ivSize == 0 { return errors.UnsupportedError("unsupported cipher in private key: " + strconv.Itoa(int(pk.cipher))) } - pk.iv = make([]byte, blockSize) + pk.iv = make([]byte, ivSize) _, err = readFull(r, pk.iv) if err != nil { return } + if v5 && pk.s2kType == S2KAEAD { + pk.iv = pk.iv[:pk.aead.IvLength()] + } } var privateKeyData []byte @@ -230,7 +311,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { return } } else { - privateKeyData, err = ioutil.ReadAll(r) + privateKeyData, err = io.ReadAll(r) if err != nil { return } @@ -239,16 +320,22 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { if len(privateKeyData) < 2 { return errors.StructuralError("truncated private key data") } - var sum uint16 - for i := 0; i < len(privateKeyData)-2; i++ { - sum += uint16(privateKeyData[i]) - } - if privateKeyData[len(privateKeyData)-2] != uint8(sum>>8) || - privateKeyData[len(privateKeyData)-1] != uint8(sum) { - return errors.StructuralError("private key checksum failure") + if pk.Version != 6 { + // checksum + var sum uint16 + for i := 0; i < len(privateKeyData)-2; i++ { + sum += uint16(privateKeyData[i]) + } + if privateKeyData[len(privateKeyData)-2] != uint8(sum>>8) || + privateKeyData[len(privateKeyData)-1] != uint8(sum) { + return errors.StructuralError("private key checksum failure") + } + privateKeyData = privateKeyData[:len(privateKeyData)-2] + return pk.parsePrivateKey(privateKeyData) + } else { + // No checksum + return pk.parsePrivateKey(privateKeyData) } - privateKeyData = privateKeyData[:len(privateKeyData)-2] - return pk.parsePrivateKey(privateKeyData) } pk.encryptedData = privateKeyData @@ -280,18 +367,59 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) { optional := bytes.NewBuffer(nil) if pk.Encrypted || pk.Dummy() { - optional.Write([]byte{uint8(pk.cipher)}) - if err := pk.s2kParams.Serialize(optional); err != nil { + // [Optional] If string-to-key usage octet was 255, 254, or 253, + // a one-octet symmetric encryption algorithm. + if _, err = optional.Write([]byte{uint8(pk.cipher)}); err != nil { + return + } + // [Optional] If string-to-key usage octet was 253, + // a one-octet AEAD algorithm. + if pk.s2kType == S2KAEAD { + if _, err = optional.Write([]byte{uint8(pk.aead)}); err != nil { + return + } + } + + s2kBuffer := bytes.NewBuffer(nil) + if err := pk.s2kParams.Serialize(s2kBuffer); err != nil { return err } + // [Optional] Only for a version 6 packet, and if string-to-key + // usage octet was 255, 254, or 253, an one-octet + // count of the following field. + if pk.Version == 6 { + if _, err = optional.Write([]byte{uint8(s2kBuffer.Len())}); err != nil { + return + } + } + // [Optional] If string-to-key usage octet was 255, 254, or 253, + // a string-to-key (S2K) specifier. The length of the string-to-key specifier + // depends on its type + if _, err = io.Copy(optional, s2kBuffer); err != nil { + return + } + + // IV if pk.Encrypted { - optional.Write(pk.iv) + if _, err = optional.Write(pk.iv); err != nil { + return + } + if pk.Version == 5 && pk.s2kType == S2KAEAD { + // Add padding for version 5 + padding := make([]byte, pk.cipher.blockSize()-len(pk.iv)) + if _, err = optional.Write(padding); err != nil { + return + } + } } } - if pk.Version == 5 { + if pk.Version == 5 || (pk.Version == 6 && pk.s2kType != S2KNON) { contents.Write([]byte{uint8(optional.Len())}) } - io.Copy(contents, optional) + + if _, err := io.Copy(contents, optional); err != nil { + return err + } if !pk.Dummy() { l := 0 @@ -303,8 +431,10 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) { return err } l = buf.Len() - checksum := mod64kHash(buf.Bytes()) - buf.Write([]byte{byte(checksum >> 8), byte(checksum)}) + if pk.Version != 6 { + checksum := mod64kHash(buf.Bytes()) + buf.Write([]byte{byte(checksum >> 8), byte(checksum)}) + } priv = buf.Bytes() } else { priv, l = pk.encryptedData, len(pk.encryptedData) @@ -370,6 +500,26 @@ func serializeECDHPrivateKey(w io.Writer, priv *ecdh.PrivateKey) error { return err } +func serializeX25519PrivateKey(w io.Writer, priv *x25519.PrivateKey) error { + _, err := w.Write(priv.Secret) + return err +} + +func serializeX448PrivateKey(w io.Writer, priv *x448.PrivateKey) error { + _, err := w.Write(priv.Secret) + return err +} + +func serializeEd25519PrivateKey(w io.Writer, priv *ed25519.PrivateKey) error { + _, err := w.Write(priv.MarshalByteSecret()) + return err +} + +func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error { + _, err := w.Write(priv.MarshalByteSecret()) + return err +} + // decrypt decrypts an encrypted private key using a decryption key. func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { @@ -378,37 +528,51 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if !pk.Encrypted { return nil } - block := pk.cipher.new(decryptionKey) - cfb := cipher.NewCFBDecrypter(block, pk.iv) - - data := make([]byte, len(pk.encryptedData)) - cfb.XORKeyStream(data, pk.encryptedData) - - if pk.sha1Checksum { - if len(data) < sha1.Size { - return errors.StructuralError("truncated private key data") - } - h := sha1.New() - h.Write(data[:len(data)-sha1.Size]) - sum := h.Sum(nil) - if !bytes.Equal(sum, data[len(data)-sha1.Size:]) { - return errors.StructuralError("private key checksum failure") - } - data = data[:len(data)-sha1.Size] - } else { - if len(data) < 2 { - return errors.StructuralError("truncated private key data") + var data []byte + switch pk.s2kType { + case S2KAEAD: + aead := pk.aead.new(block) + additionalData, err := pk.additionalData() + if err != nil { + return err } - var sum uint16 - for i := 0; i < len(data)-2; i++ { - sum += uint16(data[i]) + // Decrypt the encrypted key material with aead + data, err = aead.Open(nil, pk.iv, pk.encryptedData, additionalData) + if err != nil { + return err } - if data[len(data)-2] != uint8(sum>>8) || - data[len(data)-1] != uint8(sum) { - return errors.StructuralError("private key checksum failure") + case S2KSHA1, S2KCHECKSUM: + cfb := cipher.NewCFBDecrypter(block, pk.iv) + data = make([]byte, len(pk.encryptedData)) + cfb.XORKeyStream(data, pk.encryptedData) + if pk.s2kType == S2KSHA1 { + if len(data) < sha1.Size { + return errors.StructuralError("truncated private key data") + } + h := sha1.New() + h.Write(data[:len(data)-sha1.Size]) + sum := h.Sum(nil) + if !bytes.Equal(sum, data[len(data)-sha1.Size:]) { + return errors.StructuralError("private key checksum failure") + } + data = data[:len(data)-sha1.Size] + } else { + if len(data) < 2 { + return errors.StructuralError("truncated private key data") + } + var sum uint16 + for i := 0; i < len(data)-2; i++ { + sum += uint16(data[i]) + } + if data[len(data)-2] != uint8(sum>>8) || + data[len(data)-1] != uint8(sum) { + return errors.StructuralError("private key checksum failure") + } + data = data[:len(data)-2] } - data = data[:len(data)-2] + default: + return errors.InvalidArgumentError("invalid s2k type") } err := pk.parsePrivateKey(data) @@ -424,7 +588,6 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error { pk.s2k = nil pk.Encrypted = false pk.encryptedData = nil - return nil } @@ -440,6 +603,9 @@ func (pk *PrivateKey) decryptWithCache(passphrase []byte, keyCache *s2k.Cache) e if err != nil { return err } + if pk.s2kType == S2KAEAD { + key = pk.applyHKDF(key) + } return pk.decrypt(key) } @@ -454,11 +620,14 @@ func (pk *PrivateKey) Decrypt(passphrase []byte) error { key := make([]byte, pk.cipher.KeySize()) pk.s2k(key, passphrase) + if pk.s2kType == S2KAEAD { + key = pk.applyHKDF(key) + } return pk.decrypt(key) } // DecryptPrivateKeys decrypts all encrypted keys with the given config and passphrase. -// Avoids recomputation of similar s2k key derivations. +// Avoids recomputation of similar s2k key derivations. func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error { // Create a cache to avoid recomputation of key derviations for the same passphrase. s2kCache := &s2k.Cache{} @@ -474,7 +643,7 @@ func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error { } // encrypt encrypts an unencrypted private key. -func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction CipherFunction) error { +func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, s2kType S2KType, cipherFunction CipherFunction, rand io.Reader) error { if pk.Dummy() { return errors.ErrDummyPrivateKey("dummy key found") } @@ -485,7 +654,7 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip if len(key) != cipherFunction.KeySize() { return errors.InvalidArgumentError("supplied encryption key has the wrong size") } - + priv := bytes.NewBuffer(nil) err := pk.serializePrivateKey(priv) if err != nil { @@ -497,35 +666,53 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip pk.s2k, err = pk.s2kParams.Function() if err != nil { return err - } + } privateKeyBytes := priv.Bytes() - pk.sha1Checksum = true + pk.s2kType = s2kType block := pk.cipher.new(key) - pk.iv = make([]byte, pk.cipher.blockSize()) - _, err = rand.Read(pk.iv) - if err != nil { - return err - } - cfb := cipher.NewCFBEncrypter(block, pk.iv) - - if pk.sha1Checksum { - pk.s2kType = S2KSHA1 - h := sha1.New() - h.Write(privateKeyBytes) - sum := h.Sum(nil) - privateKeyBytes = append(privateKeyBytes, sum...) - } else { - pk.s2kType = S2KCHECKSUM - var sum uint16 - for _, b := range privateKeyBytes { - sum += uint16(b) + switch s2kType { + case S2KAEAD: + if pk.aead == 0 { + return errors.StructuralError("aead mode is not set on key") + } + aead := pk.aead.new(block) + additionalData, err := pk.additionalData() + if err != nil { + return err } - priv.Write([]byte{uint8(sum >> 8), uint8(sum)}) + pk.iv = make([]byte, aead.NonceSize()) + _, err = io.ReadFull(rand, pk.iv) + if err != nil { + return err + } + // Decrypt the encrypted key material with aead + pk.encryptedData = aead.Seal(nil, pk.iv, privateKeyBytes, additionalData) + case S2KSHA1, S2KCHECKSUM: + pk.iv = make([]byte, pk.cipher.blockSize()) + _, err = io.ReadFull(rand, pk.iv) + if err != nil { + return err + } + cfb := cipher.NewCFBEncrypter(block, pk.iv) + if s2kType == S2KSHA1 { + h := sha1.New() + h.Write(privateKeyBytes) + sum := h.Sum(nil) + privateKeyBytes = append(privateKeyBytes, sum...) + } else { + var sum uint16 + for _, b := range privateKeyBytes { + sum += uint16(b) + } + privateKeyBytes = append(privateKeyBytes, []byte{uint8(sum >> 8), uint8(sum)}...) + } + pk.encryptedData = make([]byte, len(privateKeyBytes)) + cfb.XORKeyStream(pk.encryptedData, privateKeyBytes) + default: + return errors.InvalidArgumentError("invalid s2k type for encryption") } - pk.encryptedData = make([]byte, len(privateKeyBytes)) - cfb.XORKeyStream(pk.encryptedData, privateKeyBytes) pk.Encrypted = true pk.PrivateKey = nil return err @@ -544,8 +731,15 @@ func (pk *PrivateKey) EncryptWithConfig(passphrase []byte, config *Config) error return err } s2k(key, passphrase) + s2kType := S2KSHA1 + if config.AEAD() != nil { + s2kType = S2KAEAD + pk.aead = config.AEAD().Mode() + pk.cipher = config.Cipher() + key = pk.applyHKDF(key) + } // Encrypt the private key with the derived encryption key. - return pk.encrypt(key, params, config.Cipher()) + return pk.encrypt(key, params, s2kType, config.Cipher(), config.Random()) } // EncryptPrivateKeys encrypts all unencrypted keys with the given config and passphrase. @@ -564,7 +758,16 @@ func EncryptPrivateKeys(keys []*PrivateKey, passphrase []byte, config *Config) e s2k(encryptionKey, passphrase) for _, key := range keys { if key != nil && !key.Dummy() && !key.Encrypted { - err = key.encrypt(encryptionKey, params, config.Cipher()) + s2kType := S2KSHA1 + if config.AEAD() != nil { + s2kType = S2KAEAD + key.aead = config.AEAD().Mode() + key.cipher = config.Cipher() + derivedKey := key.applyHKDF(encryptionKey) + err = key.encrypt(derivedKey, params, s2kType, config.Cipher(), config.Random()) + } else { + err = key.encrypt(encryptionKey, params, s2kType, config.Cipher(), config.Random()) + } if err != nil { return err } @@ -581,7 +784,7 @@ func (pk *PrivateKey) Encrypt(passphrase []byte) error { S2KMode: s2k.IteratedSaltedS2K, S2KCount: 65536, Hash: crypto.SHA256, - } , + }, DefaultCipher: CipherAES256, } return pk.EncryptWithConfig(passphrase, config) @@ -601,6 +804,14 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeEdDSAPrivateKey(w, priv) case *ecdh.PrivateKey: err = serializeECDHPrivateKey(w, priv) + case *x25519.PrivateKey: + err = serializeX25519PrivateKey(w, priv) + case *x448.PrivateKey: + err = serializeX448PrivateKey(w, priv) + case *ed25519.PrivateKey: + err = serializeEd25519PrivateKey(w, priv) + case *ed448.PrivateKey: + err = serializeEd448PrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -621,8 +832,18 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseECDHPrivateKey(data) case PubKeyAlgoEdDSA: return pk.parseEdDSAPrivateKey(data) + case PubKeyAlgoX25519: + return pk.parseX25519PrivateKey(data) + case PubKeyAlgoX448: + return pk.parseX448PrivateKey(data) + case PubKeyAlgoEd25519: + return pk.parseEd25519PrivateKey(data) + case PubKeyAlgoEd448: + return pk.parseEd448PrivateKey(data) + default: + err = errors.StructuralError("unknown private key type") + return } - panic("impossible") } func (pk *PrivateKey) parseRSAPrivateKey(data []byte) (err error) { @@ -743,6 +964,86 @@ func (pk *PrivateKey) parseECDHPrivateKey(data []byte) (err error) { return nil } +func (pk *PrivateKey) parseX25519PrivateKey(data []byte) (err error) { + publicKey := pk.PublicKey.PublicKey.(*x25519.PublicKey) + privateKey := x25519.NewPrivateKey(*publicKey) + privateKey.PublicKey = *publicKey + + privateKey.Secret = make([]byte, x25519.KeySize) + + if len(data) != x25519.KeySize { + err = errors.StructuralError("wrong x25519 key size") + return err + } + subtle.ConstantTimeCopy(1, privateKey.Secret, data) + if err = x25519.Validate(privateKey); err != nil { + return err + } + pk.PrivateKey = privateKey + return nil +} + +func (pk *PrivateKey) parseX448PrivateKey(data []byte) (err error) { + publicKey := pk.PublicKey.PublicKey.(*x448.PublicKey) + privateKey := x448.NewPrivateKey(*publicKey) + privateKey.PublicKey = *publicKey + + privateKey.Secret = make([]byte, x448.KeySize) + + if len(data) != x448.KeySize { + err = errors.StructuralError("wrong x448 key size") + return err + } + subtle.ConstantTimeCopy(1, privateKey.Secret, data) + if err = x448.Validate(privateKey); err != nil { + return err + } + pk.PrivateKey = privateKey + return nil +} + +func (pk *PrivateKey) parseEd25519PrivateKey(data []byte) (err error) { + publicKey := pk.PublicKey.PublicKey.(*ed25519.PublicKey) + privateKey := ed25519.NewPrivateKey(*publicKey) + privateKey.PublicKey = *publicKey + + if len(data) != ed25519.SeedSize { + err = errors.StructuralError("wrong ed25519 key size") + return err + } + err = privateKey.UnmarshalByteSecret(data) + if err != nil { + return err + } + err = ed25519.Validate(privateKey) + if err != nil { + return err + } + pk.PrivateKey = privateKey + return nil +} + +func (pk *PrivateKey) parseEd448PrivateKey(data []byte) (err error) { + publicKey := pk.PublicKey.PublicKey.(*ed448.PublicKey) + privateKey := ed448.NewPrivateKey(*publicKey) + privateKey.PublicKey = *publicKey + + if len(data) != ed448.SeedSize { + err = errors.StructuralError("wrong ed448 key size") + return err + } + err = privateKey.UnmarshalByteSecret(data) + if err != nil { + return err + } + err = ed448.Validate(privateKey) + if err != nil { + return err + } + pk.PrivateKey = privateKey + return nil +} + func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) { eddsaPub := pk.PublicKey.PublicKey.(*eddsa.PublicKey) eddsaPriv := eddsa.NewPrivateKey(*eddsaPub) @@ -767,6 +1068,41 @@ func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) { return nil } +func (pk *PrivateKey) additionalData() ([]byte, error) { + additionalData := bytes.NewBuffer(nil) + // Write additional data prefix based on packet type + var packetByte byte + if pk.PublicKey.IsSubkey { + packetByte = 0xc7 + } else { + packetByte = 0xc5 + } + // Write public key to additional data + _, err := additionalData.Write([]byte{packetByte}) + if err != nil { + return nil, err + } + err = pk.PublicKey.serializeWithoutHeaders(additionalData) + if err != nil { + return nil, err + } + return additionalData.Bytes(), nil +} + +func (pk *PrivateKey) applyHKDF(inputKey []byte) []byte { + var packetByte byte + if pk.PublicKey.IsSubkey { + packetByte = 0xc7 + } else { + packetByte = 0xc5 + } + associatedData := []byte{packetByte, byte(pk.Version), byte(pk.cipher), byte(pk.aead)} + hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData) + encryptionKey := make([]byte, pk.cipher.KeySize()) + _, _ = readFull(hkdfReader, encryptionKey) + return encryptionKey +} + func validateDSAParameters(priv *dsa.PrivateKey) error { p := priv.P // group prime q := priv.Q // subgroup order diff --git a/openpgp/packet/private_key_test.go b/openpgp/packet/private_key_test.go index 154766f94..46cf67aea 100644 --- a/openpgp/packet/private_key_test.go +++ b/openpgp/packet/private_key_test.go @@ -13,6 +13,7 @@ import ( "crypto/rsa" "crypto/x509" "encoding/hex" + "fmt" "hash" "math/big" mathrand "math/rand" @@ -138,67 +139,80 @@ func TestExternalPrivateKeyEncryptDecryptRandomizeSlow(t *testing.T) { } } -func TestExternalPrivateKeyEncryptDecryptArgon2(t *testing.T) { - config := &Config{ - S2KConfig: &s2k.Config{S2KMode: s2k.Argon2S2K}, - } - for i, test := range privateKeyTests { - packet, err := Read(readerFromHex(test.privateKeyHex)) - if err != nil { - t.Errorf("#%d: failed to parse: %s", i, err) - continue - } - - privKey := packet.(*PrivateKey) - - if !privKey.Encrypted { - t.Errorf("#%d: private key isn't encrypted", i) - continue - } - - // Decrypt with the correct password - err = privKey.Decrypt([]byte("testing")) - if err != nil { - t.Errorf("#%d: failed to decrypt: %s", i, err) - continue - } - - // Encrypt with another (possibly empty) password - randomPassword := make([]byte, mathrand.Intn(30)) - rand.Read(randomPassword) - err = privKey.EncryptWithConfig(randomPassword, config) - if err != nil { - t.Errorf("#%d: failed to encrypt: %s", i, err) - continue - } - - // Try to decrypt with incorrect password - incorrect := make([]byte, 1+mathrand.Intn(30)) - for rand.Read(incorrect); bytes.Equal(incorrect, randomPassword); { - rand.Read(incorrect) - } - err = privKey.Decrypt(incorrect) - if err == nil { - t.Errorf("#%d: decrypted with incorrect password\nPassword is:%vDecrypted with:%v", i, randomPassword, incorrect) - continue - } - - // Try to decrypt with old password - err = privKey.Decrypt([]byte("testing")) - if err == nil { - t.Errorf("#%d: decrypted with old password", i) - continue - } - - // Decrypt with correct password - err = privKey.Decrypt(randomPassword) - if err != nil { - t.Errorf("#%d: failed to decrypt: %s", i, err) - continue - } - - if !privKey.CreationTime.Equal(test.creationTime) || privKey.Encrypted { - t.Errorf("#%d: bad result, got: %#v", i, privKey) +func TestExternalPrivateKeyEncryptDecryptS2KModes(t *testing.T) { + sk2Modes := []s2k.Mode{s2k.IteratedSaltedS2K, s2k.Argon2S2K} + sk2KeyTypes := []S2KType{S2KAEAD, S2KSHA1} + for _, s2kMode := range sk2Modes { + for _, sk2KeyType := range sk2KeyTypes { + t.Run(fmt.Sprintf("s2kMode:%d-s2kType:%d", s2kMode, sk2KeyType), func(t *testing.T) { + var configAEAD *AEADConfig + if sk2KeyType == S2KAEAD { + configAEAD = &AEADConfig{} + } + config := &Config{ + S2KConfig: &s2k.Config{S2KMode: s2kMode}, + AEADConfig: configAEAD, + } + for i, test := range privateKeyTests { + packet, err := Read(readerFromHex(test.privateKeyHex)) + if err != nil { + t.Errorf("#%d: failed to parse: %s", i, err) + continue + } + + privKey := packet.(*PrivateKey) + + if !privKey.Encrypted { + t.Errorf("#%d: private key isn't encrypted", i) + continue + } + + // Decrypt with the correct password + err = privKey.Decrypt([]byte("testing")) + if err != nil { + t.Errorf("#%d: failed to decrypt: %s", i, err) + continue + } + + // Encrypt with another (possibly empty) password + randomPassword := make([]byte, mathrand.Intn(30)) + rand.Read(randomPassword) + err = privKey.EncryptWithConfig(randomPassword, config) + if err != nil { + t.Errorf("#%d: failed to encrypt: %s", i, err) + continue + } + + // Try to decrypt with incorrect password + incorrect := make([]byte, 1+mathrand.Intn(30)) + for rand.Read(incorrect); bytes.Equal(incorrect, randomPassword); { + rand.Read(incorrect) + } + err = privKey.Decrypt(incorrect) + if err == nil { + t.Errorf("#%d: decrypted with incorrect password\nPassword is:%vDecrypted with:%v", i, randomPassword, incorrect) + continue + } + + // Try to decrypt with old password + err = privKey.Decrypt([]byte("testing")) + if err == nil { + t.Errorf("#%d: decrypted with old password", i) + continue + } + + // Decrypt with correct password + err = privKey.Decrypt(randomPassword) + if err != nil { + t.Errorf("#%d: failed to decrypt: %s", i, err) + continue + } + + if !privKey.CreationTime.Equal(test.creationTime) || privKey.Encrypted { + t.Errorf("#%d: bad result, got: %#v", i, privKey) + } + } + }) } } } @@ -449,9 +463,13 @@ func TestEncryptDecryptEdDSAPrivateKeyRandomizeFast(t *testing.T) { copy(copiedSecret, privKey.PrivateKey.(*eddsa.PrivateKey).D) // Encrypt private key with random passphrase - privKey.Encrypt(password) + if err := privKey.Encrypt(password); err != nil { + t.Fatal(err) + } // Decrypt and check correctness - privKey.Decrypt(password) + if err := privKey.Decrypt(password); err != nil { + t.Fatal(err) + } decryptedSecret := privKey.PrivateKey.(*eddsa.PrivateKey).D if !bytes.Equal(decryptedSecret, copiedSecret) { diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 3402b8c14..dd93c9870 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,7 +5,6 @@ package packet import ( - "crypto" "crypto/dsa" "crypto/rsa" "crypto/sha1" @@ -21,23 +20,24 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/ed25519" + "github.com/ProtonMail/go-crypto/openpgp/ed448" "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/x25519" + "github.com/ProtonMail/go-crypto/openpgp/x448" ) -type kdfHashFunction byte -type kdfAlgorithm byte - // PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2. type PublicKey struct { Version int CreationTime time.Time PubKeyAlgo PublicKeyAlgorithm - PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey + PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey Fingerprint []byte KeyId uint64 IsSubkey bool @@ -61,11 +61,18 @@ func (pk *PublicKey) UpgradeToV5() { pk.setFingerprintAndKeyId() } +// UpgradeToV6 updates the version of the key to v6, and updates all necessary +// fields. +func (pk *PublicKey) UpgradeToV6() { + pk.Version = 6 + pk.setFingerprintAndKeyId() +} + // signingKey provides a convenient abstraction over signature verification // for v3 and v4 public keys. type signingKey interface { SerializeForHash(io.Writer) error - SerializeSignaturePrefix(io.Writer) + SerializeSignaturePrefix(io.Writer) error serializeWithoutHeaders(io.Writer) error } @@ -174,6 +181,54 @@ func NewEdDSAPublicKey(creationTime time.Time, pub *eddsa.PublicKey) *PublicKey return pk } +func NewX25519PublicKey(creationTime time.Time, pub *x25519.PublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoX25519, + PublicKey: pub, + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewX448PublicKey(creationTime time.Time, pub *x448.PublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoX448, + PublicKey: pub, + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewEd25519PublicKey(creationTime time.Time, pub *ed25519.PublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoEd25519, + PublicKey: pub, + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoEd448, + PublicKey: pub, + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -181,12 +236,14 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { if err != nil { return } - if buf[0] != 4 && buf[0] != 5 { + if buf[0] != 4 && buf[0] != 5 && buf[0] != 6 { return errors.UnsupportedError("public key version " + strconv.Itoa(int(buf[0]))) } pk.Version = int(buf[0]) - if pk.Version == 5 { + if pk.Version >= 5 { + // Read the four-octet scalar octet count + // The count is not used in this implementation var n [4]byte _, err = readFull(r, n[:]) if err != nil { @@ -195,6 +252,7 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { } pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0) pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5]) + // Ignore four-ocet length switch pk.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: err = pk.parseRSA(r) @@ -208,6 +266,14 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseECDH(r) case PubKeyAlgoEdDSA: err = pk.parseEdDSA(r) + case PubKeyAlgoX25519: + err = pk.parseX25519(r) + case PubKeyAlgoX448: + err = pk.parseX448(r) + case PubKeyAlgoEd25519: + err = pk.parseEd25519(r) + case PubKeyAlgoEd448: + err = pk.parseEd448(r) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -221,15 +287,21 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { func (pk *PublicKey) setFingerprintAndKeyId() { // RFC 4880, section 12.2 - if pk.Version == 5 { + if pk.Version >= 5 { fingerprint := sha256.New() - pk.SerializeForHash(fingerprint) + if err := pk.SerializeForHash(fingerprint); err != nil { + // Should not happen for a hash. + panic(err) + } pk.Fingerprint = make([]byte, 32) copy(pk.Fingerprint, fingerprint.Sum(nil)) pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[:8]) } else { fingerprint := sha1.New() - pk.SerializeForHash(fingerprint) + if err := pk.SerializeForHash(fingerprint); err != nil { + // Should not happen for a hash. + panic(err) + } pk.Fingerprint = make([]byte, 20) copy(pk.Fingerprint, fingerprint.Sum(nil)) pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[12:20]) @@ -324,16 +396,17 @@ func (pk *PublicKey) parseECDSA(r io.Reader) (err error) { if _, err = pk.oid.ReadFrom(r); err != nil { return } - pk.p = new(encoding.MPI) - if _, err = pk.p.ReadFrom(r); err != nil { - return - } curveInfo := ecc.FindByOid(pk.oid) if curveInfo == nil { return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid)) } + pk.p = new(encoding.MPI) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + c, ok := curveInfo.Curve.(ecc.ECDSACurve) if !ok { return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid)) @@ -353,6 +426,12 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { if _, err = pk.oid.ReadFrom(r); err != nil { return } + + curveInfo := ecc.FindByOid(pk.oid) + if curveInfo == nil { + return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid)) + } + pk.p = new(encoding.MPI) if _, err = pk.p.ReadFrom(r); err != nil { return @@ -362,12 +441,6 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return } - curveInfo := ecc.FindByOid(pk.oid) - - if curveInfo == nil { - return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid)) - } - c, ok := curveInfo.Curve.(ecc.ECDHCurve) if !ok { return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid)) @@ -400,6 +473,7 @@ func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) { if _, err = pk.oid.ReadFrom(r); err != nil { return } + curveInfo := ecc.FindByOid(pk.oid) if curveInfo == nil { return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid)) @@ -435,75 +509,148 @@ func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) { return } +func (pk *PublicKey) parseX25519(r io.Reader) (err error) { + point := make([]byte, x25519.KeySize) + _, err = io.ReadFull(r, point) + if err != nil { + return + } + pub := &x25519.PublicKey{ + Point: point, + } + pk.PublicKey = pub + return +} + +func (pk *PublicKey) parseX448(r io.Reader) (err error) { + point := make([]byte, x448.KeySize) + _, err = io.ReadFull(r, point) + if err != nil { + return + } + pub := &x448.PublicKey{ + Point: point, + } + pk.PublicKey = pub + return +} + +func (pk *PublicKey) parseEd25519(r io.Reader) (err error) { + point := make([]byte, ed25519.PublicKeySize) + _, err = io.ReadFull(r, point) + if err != nil { + return + } + pub := &ed25519.PublicKey{ + Point: point, + } + pk.PublicKey = pub + return +} + +func (pk *PublicKey) parseEd448(r io.Reader) (err error) { + point := make([]byte, ed448.PublicKeySize) + _, err = io.ReadFull(r, point) + if err != nil { + return + } + pub := &ed448.PublicKey{ + Point: point, + } + pk.PublicKey = pub + return +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { - pk.SerializeSignaturePrefix(w) + if err := pk.SerializeSignaturePrefix(w); err != nil { + return err + } return pk.serializeWithoutHeaders(w) } // SerializeSignaturePrefix writes the prefix for this public key to the given Writer. // The prefix is used when calculating a signature over this public key. See // RFC 4880, section 5.2.4. -func (pk *PublicKey) SerializeSignaturePrefix(w io.Writer) { +func (pk *PublicKey) SerializeSignaturePrefix(w io.Writer) error { var pLength = pk.algorithmSpecificByteCount() - if pk.Version == 5 { - pLength += 10 // version, timestamp (4), algorithm, key octet count (4). - w.Write([]byte{ - 0x9A, + // version, timestamp, algorithm + pLength += versionSize + timestampSize + algorithmSize + if pk.Version >= 5 { + // key octet count (4). + pLength += 4 + _, err := w.Write([]byte{ + // When a v4 signature is made over a key, the hash data starts with the octet 0x99, followed by a two-octet length + // of the key, and then the body of the key packet. When a v6 signature is made over a key, the hash data starts + // with the salt, then octet 0x9B, followed by a four-octet length of the key, and then the body of the key packet. + 0x95 + byte(pk.Version), byte(pLength >> 24), byte(pLength >> 16), byte(pLength >> 8), byte(pLength), }) - return + if err != nil { + return err + } + return nil + } + if _, err := w.Write([]byte{0x99, byte(pLength >> 8), byte(pLength)}); err != nil { + return err } - pLength += 6 - w.Write([]byte{0x99, byte(pLength >> 8), byte(pLength)}) + return nil } func (pk *PublicKey) Serialize(w io.Writer) (err error) { - length := 6 // 6 byte header + length := uint32(versionSize + timestampSize + algorithmSize) // 6 byte header length += pk.algorithmSpecificByteCount() - if pk.Version == 5 { + if pk.Version >= 5 { length += 4 // octet key count } packetType := packetTypePublicKey if pk.IsSubkey { packetType = packetTypePublicSubkey } - err = serializeHeader(w, packetType, length) + err = serializeHeader(w, packetType, int(length)) if err != nil { return } return pk.serializeWithoutHeaders(w) } -func (pk *PublicKey) algorithmSpecificByteCount() int { - length := 0 +func (pk *PublicKey) algorithmSpecificByteCount() uint32 { + length := uint32(0) switch pk.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: - length += int(pk.n.EncodedLength()) - length += int(pk.e.EncodedLength()) + length += uint32(pk.n.EncodedLength()) + length += uint32(pk.e.EncodedLength()) case PubKeyAlgoDSA: - length += int(pk.p.EncodedLength()) - length += int(pk.q.EncodedLength()) - length += int(pk.g.EncodedLength()) - length += int(pk.y.EncodedLength()) + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.q.EncodedLength()) + length += uint32(pk.g.EncodedLength()) + length += uint32(pk.y.EncodedLength()) case PubKeyAlgoElGamal: - length += int(pk.p.EncodedLength()) - length += int(pk.g.EncodedLength()) - length += int(pk.y.EncodedLength()) + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.g.EncodedLength()) + length += uint32(pk.y.EncodedLength()) case PubKeyAlgoECDSA: - length += int(pk.oid.EncodedLength()) - length += int(pk.p.EncodedLength()) + length += uint32(pk.oid.EncodedLength()) + length += uint32(pk.p.EncodedLength()) case PubKeyAlgoECDH: - length += int(pk.oid.EncodedLength()) - length += int(pk.p.EncodedLength()) - length += int(pk.kdf.EncodedLength()) + length += uint32(pk.oid.EncodedLength()) + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.kdf.EncodedLength()) case PubKeyAlgoEdDSA: - length += int(pk.oid.EncodedLength()) - length += int(pk.p.EncodedLength()) + length += uint32(pk.oid.EncodedLength()) + length += uint32(pk.p.EncodedLength()) + case PubKeyAlgoX25519: + length += x25519.KeySize + case PubKeyAlgoX448: + length += x448.KeySize + case PubKeyAlgoEd25519: + length += ed25519.PublicKeySize + case PubKeyAlgoEd448: + length += ed448.PublicKeySize default: panic("unknown public key algorithm") } @@ -522,7 +669,7 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { return } - if pk.Version == 5 { + if pk.Version >= 5 { n := pk.algorithmSpecificByteCount() if _, err = w.Write([]byte{ byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n), @@ -580,6 +727,22 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(pk.p.EncodedBytes()) return + case PubKeyAlgoX25519: + publicKey := pk.PublicKey.(*x25519.PublicKey) + _, err = w.Write(publicKey.Point) + return + case PubKeyAlgoX448: + publicKey := pk.PublicKey.(*x448.PublicKey) + _, err = w.Write(publicKey.Point) + return + case PubKeyAlgoEd25519: + publicKey := pk.PublicKey.(*ed25519.PublicKey) + _, err = w.Write(publicKey.Point) + return + case PubKeyAlgoEd448: + publicKey := pk.PublicKey.(*ed448.PublicKey) + _, err = w.Write(publicKey.Point) + return } return errors.InvalidArgumentError("bad public-key algorithm") } @@ -600,7 +763,8 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro } signed.Write(sig.HashSuffix) hashBytes := signed.Sum(nil) - if sig.Version == 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) { + // see discussion https://github.com/ProtonMail/go-crypto/issues/107 + if sig.Version >= 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) { return errors.SignatureError("hash tag doesn't match") } @@ -639,6 +803,18 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("EdDSA verification failure") } return nil + case PubKeyAlgoEd25519: + ed25519PublicKey := pk.PublicKey.(*ed25519.PublicKey) + if !ed25519.Verify(ed25519PublicKey, hashBytes, sig.EdSig) { + return errors.SignatureError("Ed25519 verification failure") + } + return nil + case PubKeyAlgoEd448: + ed448PublicKey := pk.PublicKey.(*ed448.PublicKey) + if !ed448.Verify(ed448PublicKey, hashBytes, sig.EdSig) { + return errors.SignatureError("ed448 verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -646,11 +822,8 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro // keySignatureHash returns a Hash of the message that needs to be signed for // pk to assert a subkey relationship to signed. -func keySignatureHash(pk, signed signingKey, hashFunc crypto.Hash) (h hash.Hash, err error) { - if !hashFunc.Available() { - return nil, errors.UnsupportedError("hash function") - } - h = hashFunc.New() +func keySignatureHash(pk, signed signingKey, hashFunc hash.Hash) (h hash.Hash, err error) { + h = hashFunc // RFC 4880, section 5.2.4 err = pk.SerializeForHash(h) @@ -665,7 +838,11 @@ func keySignatureHash(pk, signed signingKey, hashFunc crypto.Hash) (h hash.Hash, // VerifyKeySignature returns nil iff sig is a valid signature, made by this // public key, of signed. func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error { - h, err := keySignatureHash(pk, signed, sig.Hash) + preparedHash, err := sig.PrepareVerify() + if err != nil { + return err + } + h, err := keySignatureHash(pk, signed, preparedHash) if err != nil { return err } @@ -679,10 +856,14 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error if sig.EmbeddedSignature == nil { return errors.StructuralError("signing subkey is missing cross-signature") } + preparedHashEmbedded, err := sig.EmbeddedSignature.PrepareVerify() + if err != nil { + return err + } // Verify the cross-signature. This is calculated over the same // data as the main signature, so we cannot just recursively // call signed.VerifyKeySignature(...) - if h, err = keySignatureHash(pk, signed, sig.EmbeddedSignature.Hash); err != nil { + if h, err = keySignatureHash(pk, signed, preparedHashEmbedded); err != nil { return errors.StructuralError("error while hashing for cross-signature: " + err.Error()) } if err := signed.VerifySignature(h, sig.EmbeddedSignature); err != nil { @@ -693,32 +874,31 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error return nil } -func keyRevocationHash(pk signingKey, hashFunc crypto.Hash) (h hash.Hash, err error) { - if !hashFunc.Available() { - return nil, errors.UnsupportedError("hash function") - } - h = hashFunc.New() - - // RFC 4880, section 5.2.4 - err = pk.SerializeForHash(h) - - return +func keyRevocationHash(pk signingKey, hashFunc hash.Hash) (err error) { + return pk.SerializeForHash(hashFunc) } // VerifyRevocationSignature returns nil iff sig is a valid signature, made by this // public key. func (pk *PublicKey) VerifyRevocationSignature(sig *Signature) (err error) { - h, err := keyRevocationHash(pk, sig.Hash) + preparedHash, err := sig.PrepareVerify() if err != nil { return err } - return pk.VerifySignature(h, sig) + if keyRevocationHash(pk, preparedHash); err != nil { + return err + } + return pk.VerifySignature(preparedHash, sig) } // VerifySubkeyRevocationSignature returns nil iff sig is a valid subkey revocation signature, // made by this public key, of signed. func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *PublicKey) (err error) { - h, err := keySignatureHash(pk, signed, sig.Hash) + preparedHash, err := sig.PrepareVerify() + if err != nil { + return err + } + h, err := keySignatureHash(pk, signed, preparedHash) if err != nil { return err } @@ -727,15 +907,15 @@ func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *Pub // userIdSignatureHash returns a Hash of the message that needs to be signed // to assert that pk is a valid key for id. -func userIdSignatureHash(id string, pk *PublicKey, hashFunc crypto.Hash) (h hash.Hash, err error) { - if !hashFunc.Available() { - return nil, errors.UnsupportedError("hash function") - } - h = hashFunc.New() +func userIdSignatureHash(id string, pk *PublicKey, h hash.Hash) (err error) { // RFC 4880, section 5.2.4 - pk.SerializeSignaturePrefix(h) - pk.serializeWithoutHeaders(h) + if err := pk.SerializeSignaturePrefix(h); err != nil { + return err + } + if err := pk.serializeWithoutHeaders(h); err != nil { + return err + } var buf [5]byte buf[0] = 0xb4 @@ -746,16 +926,37 @@ func userIdSignatureHash(id string, pk *PublicKey, hashFunc crypto.Hash) (h hash h.Write(buf[:]) h.Write([]byte(id)) - return + return nil +} + +// directKeySignatureHash returns a Hash of the message that needs to be signed. +func directKeySignatureHash(pk *PublicKey, h hash.Hash) (err error) { + return pk.SerializeForHash(h) } // VerifyUserIdSignature returns nil iff sig is a valid signature, made by this // public key, that id is the identity of pub. func (pk *PublicKey) VerifyUserIdSignature(id string, pub *PublicKey, sig *Signature) (err error) { - h, err := userIdSignatureHash(id, pub, sig.Hash) + h, err := sig.PrepareVerify() + if err != nil { + return err + } + if err := userIdSignatureHash(id, pub, h); err != nil { + return err + } + return pk.VerifySignature(h, sig) +} + +// VerifyDirectKeySignature returns nil iff sig is a valid signature, made by this +// public key. +func (pk *PublicKey) VerifyDirectKeySignature(sig *Signature) (err error) { + h, err := sig.PrepareVerify() if err != nil { return err } + if err := directKeySignatureHash(pk, h); err != nil { + return err + } return pk.VerifySignature(h, sig) } @@ -786,21 +987,49 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = pk.p.BitLength() case PubKeyAlgoEdDSA: bitLength = pk.p.BitLength() + case PubKeyAlgoX25519: + bitLength = x25519.KeySize * 8 + case PubKeyAlgoX448: + bitLength = x448.KeySize * 8 + case PubKeyAlgoEd25519: + bitLength = ed25519.PublicKeySize * 8 + case PubKeyAlgoEd448: + bitLength = ed448.PublicKeySize * 8 default: err = errors.InvalidArgumentError("bad public-key algorithm") } return } +// Curve returns the used elliptic curve of this public key. +// Returns an error if no elliptic curve is used. +func (pk *PublicKey) Curve() (curve Curve, err error) { + switch pk.PubKeyAlgo { + case PubKeyAlgoECDSA, PubKeyAlgoECDH, PubKeyAlgoEdDSA: + curveInfo := ecc.FindByOid(pk.oid) + if curveInfo == nil { + return "", errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid)) + } + curve = Curve(curveInfo.GenName) + case PubKeyAlgoEd25519, PubKeyAlgoX25519: + curve = Curve25519 + case PubKeyAlgoEd448, PubKeyAlgoX448: + curve = Curve448 + default: + err = errors.InvalidArgumentError("public key does not operate with an elliptic curve") + } + return +} + // KeyExpired returns whether sig is a self-signature of a key that has // expired or is created in the future. func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool { - if pk.CreationTime.After(currentTime) { + if pk.CreationTime.Unix() > currentTime.Unix() { return true } if sig.KeyLifetimeSecs == nil || *sig.KeyLifetimeSecs == 0 { return false } expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second) - return currentTime.After(expiry) + return currentTime.Unix() > expiry.Unix() } diff --git a/openpgp/packet/public_key_test.go b/openpgp/packet/public_key_test.go index 41586eb83..a977e6017 100644 --- a/openpgp/packet/public_key_test.go +++ b/openpgp/packet/public_key_test.go @@ -90,7 +90,7 @@ func TestPublicKeySerialize(t *testing.T) { t.Errorf("#%d: Read error (from serialized data): %s", i, err) continue } - pk, ok = packet.(*PublicKey) + _, ok = packet.(*PublicKey) if !ok { t.Errorf("#%d: failed to parse serialized data, got: %#v", i, packet) continue diff --git a/openpgp/packet/reader.go b/openpgp/packet/reader.go index 10215fe5f..dd8409239 100644 --- a/openpgp/packet/reader.go +++ b/openpgp/packet/reader.go @@ -10,6 +10,12 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" ) +type PacketReader interface { + Next() (p Packet, err error) + Push(reader io.Reader) (err error) + Unread(p Packet) +} + // Reader reads packets from an io.Reader and allows packets to be 'unread' so // that they result from the next call to Next. type Reader struct { @@ -26,37 +32,81 @@ type Reader struct { const maxReaders = 32 // Next returns the most recently unread Packet, or reads another packet from -// the top-most io.Reader. Unknown packet types are skipped. +// the top-most io.Reader. Unknown/unsupported/Marker packet types are skipped. func (r *Reader) Next() (p Packet, err error) { + for { + p, err := r.read() + if err == io.EOF { + break + } else if err != nil { + if _, ok := err.(errors.UnknownPacketTypeError); ok { + continue + } + if _, ok := err.(errors.UnsupportedError); ok { + switch p.(type) { + case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData: + return nil, err + } + continue + } + return nil, err + } else { + //A marker packet MUST be ignored when received + switch p.(type) { + case *Marker: + continue + } + return p, nil + } + } + return nil, io.EOF +} + +// Next returns the most recently unread Packet, or reads another packet from +// the top-most io.Reader. Unknown/Marker packet types are skipped while unsupported +// packets are returned as UnsupportedPacket type. +func (r *Reader) NextWithUnsupported() (p Packet, err error) { + for { + p, err = r.read() + if err == io.EOF { + break + } else if err != nil { + if _, ok := err.(errors.UnknownPacketTypeError); ok { + continue + } + if casteErr, ok := err.(errors.UnsupportedError); ok { + return &UnsupportedPacket{ + IncompletePacket: p, + Error: casteErr, + }, nil + } + return + } else { + //A marker packet MUST be ignored when received + switch p.(type) { + case *Marker: + continue + } + return + } + } + return nil, io.EOF +} + +func (r *Reader) read() (p Packet, err error) { if len(r.q) > 0 { p = r.q[len(r.q)-1] r.q = r.q[:len(r.q)-1] return } - for len(r.readers) > 0 { p, err = Read(r.readers[len(r.readers)-1]) - if err == nil { - return - } if err == io.EOF { r.readers = r.readers[:len(r.readers)-1] continue } - // TODO: Add strict mode that rejects unknown packets, instead of ignoring them. - if _, ok := err.(errors.UnknownPacketTypeError); ok { - continue - } - if _, ok := err.(errors.UnsupportedError); ok { - switch p.(type) { - case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData: - return nil, err - } - continue - } - return nil, err + return p, err } - return nil, io.EOF } @@ -84,3 +134,76 @@ func NewReader(r io.Reader) *Reader { readers: []io.Reader{r}, } } + +// CheckReader is similar to Reader but additionally +// uses the pushdown automata to verify the read packet sequence. +type CheckReader struct { + Reader + verifier *SequenceVerifier + fullyRead bool +} + +// Next returns the most recently unread Packet, or reads another packet from +// the top-most io.Reader. Unknown packet types are skipped. +// If the read packet sequence does not conform to the packet composition +// rules in rfc4880, it returns an error. +func (r *CheckReader) Next() (p Packet, err error) { + if r.fullyRead { + return nil, io.EOF + } + if len(r.q) > 0 { + p = r.q[len(r.q)-1] + r.q = r.q[:len(r.q)-1] + return + } + var errMsg error + for len(r.readers) > 0 { + p, errMsg, err = ReadWithCheck(r.readers[len(r.readers)-1], r.verifier) + if errMsg != nil { + err = errMsg + return + } + if err == nil { + return + } + if err == io.EOF { + r.readers = r.readers[:len(r.readers)-1] + continue + } + //A marker packet MUST be ignored when received + switch p.(type) { + case *Marker: + continue + } + if _, ok := err.(errors.UnknownPacketTypeError); ok { + continue + } + if _, ok := err.(errors.UnsupportedError); ok { + switch p.(type) { + case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData: + return nil, err + } + continue + } + return nil, err + } + if errMsg = r.verifier.Next(EOSSymbol); errMsg != nil { + return nil, errMsg + } + if errMsg = r.verifier.AssertValid(); errMsg != nil { + return nil, errMsg + } + r.fullyRead = true + return nil, io.EOF +} + +func NewCheckReader(r io.Reader) *CheckReader { + return &CheckReader{ + Reader: Reader{ + q: nil, + readers: []io.Reader{r}, + }, + verifier: NewSequenceVerifier(), + fullyRead: false, + } +} diff --git a/openpgp/packet/recipient.go b/openpgp/packet/recipient.go new file mode 100644 index 000000000..fb2e362e4 --- /dev/null +++ b/openpgp/packet/recipient.go @@ -0,0 +1,15 @@ +package packet + +// Recipient type represents a Intended Recipient Fingerprint subpacket +// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-intended-recipient-fingerpr +type Recipient struct { + KeyVersion int + Fingerprint []byte +} + +func (r *Recipient) Serialize() []byte { + packet := make([]byte, len(r.Fingerprint)+1) + packet[0] = byte(r.KeyVersion) + copy(packet[1:], r.Fingerprint) + return packet +} diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 80d0bb98e..ff14da318 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -15,6 +15,8 @@ import ( "time" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/ed25519" + "github.com/ProtonMail/go-crypto/openpgp/ed448" "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" @@ -39,6 +41,9 @@ type Signature struct { SigType SignatureType PubKeyAlgo PublicKeyAlgorithm Hash crypto.Hash + // salt contains a random salt value for v6 signatures + // See RFC the crypto refresh Section 5.2.3. + salt []byte // HashSuffix is extra data that is hashed in after the signed data. HashSuffix []byte @@ -57,6 +62,7 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field + EdSig []byte // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -72,6 +78,7 @@ type Signature struct { SignerUserId *string IsPrimaryId *bool Notations []*Notation + IntendedRecipients []*Recipient // TrustLevel and TrustAmount can be set by the signer to assert that // the key is not only valid but also trustworthy at the specified @@ -113,26 +120,59 @@ type Signature struct { outSubpackets []outputSubpacket } +// VerifiableSignature internally keeps state if the +// the signature has been verified before. +type VerifiableSignature struct { + Valid *bool // nil if it has not been verified yet + Packet *Signature +} + +// SaltedHashSpecifier specifies that the given salt and hash are +// used by a v6 signature. +type SaltedHashSpecifier struct { + Hash crypto.Hash + Salt []byte +} + +// NewVerifiableSig returns a struct of type VerifiableSignature referencing the input signature. +func NewVerifiableSig(signature *Signature) *VerifiableSignature { + return &VerifiableSignature{ + Packet: signature, + } +} + +// Salt returns the signature salt for v6 signatures. +func (sig *Signature) Salt() []byte { + if sig == nil { + return nil + } + return sig.salt +} + func (sig *Signature) parse(r io.Reader) (err error) { // RFC 4880, section 5.2.3 - var buf [5]byte + var buf [7]byte _, err = readFull(r, buf[:1]) if err != nil { return } - if buf[0] != 4 && buf[0] != 5 { + if buf[0] != 4 && buf[0] != 5 && buf[0] != 6 { err = errors.UnsupportedError("signature packet version " + strconv.Itoa(int(buf[0]))) return } sig.Version = int(buf[0]) - _, err = readFull(r, buf[:5]) + if sig.Version == 6 { + _, err = readFull(r, buf[:7]) + } else { + _, err = readFull(r, buf[:5]) + } if err != nil { return } sig.SigType = SignatureType(buf[0]) sig.PubKeyAlgo = PublicKeyAlgorithm(buf[1]) switch sig.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -150,7 +190,17 @@ func (sig *Signature) parse(r io.Reader) (err error) { return errors.UnsupportedError("hash function " + strconv.Itoa(int(buf[2]))) } - hashedSubpacketsLength := int(buf[3])<<8 | int(buf[4]) + var hashedSubpacketsLength int + if sig.Version == 6 { + // For a v6 signature, a four-octet length is used. + hashedSubpacketsLength = + int(buf[3])<<24 | + int(buf[4])<<16 | + int(buf[5])<<8 | + int(buf[6]) + } else { + hashedSubpacketsLength = int(buf[3])<<8 | int(buf[4]) + } hashedSubpackets := make([]byte, hashedSubpacketsLength) _, err = readFull(r, hashedSubpackets) if err != nil { @@ -166,11 +216,21 @@ func (sig *Signature) parse(r io.Reader) (err error) { return } - _, err = readFull(r, buf[:2]) + if sig.Version == 6 { + _, err = readFull(r, buf[:4]) + } else { + _, err = readFull(r, buf[:2]) + } + if err != nil { return } - unhashedSubpacketsLength := int(buf[0])<<8 | int(buf[1]) + var unhashedSubpacketsLength uint32 + if sig.Version == 6 { + unhashedSubpacketsLength = uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3]) + } else { + unhashedSubpacketsLength = uint32(buf[0])<<8 | uint32(buf[1]) + } unhashedSubpackets := make([]byte, unhashedSubpacketsLength) _, err = readFull(r, unhashedSubpackets) if err != nil { @@ -186,6 +246,30 @@ func (sig *Signature) parse(r io.Reader) (err error) { return } + if sig.Version == 6 { + // Only for v6 signatures, a variable-length field containing the salt + _, err = readFull(r, buf[:1]) + if err != nil { + return + } + saltLength := int(buf[0]) + var expectedSaltLength int + expectedSaltLength, err = SaltLengthForHash(sig.Hash) + if err != nil { + return + } + if saltLength != expectedSaltLength { + err = errors.StructuralError("unexpected salt size for the given hash algorithm") + return + } + salt := make([]byte, expectedSaltLength) + _, err = readFull(r, salt) + if err != nil { + return + } + sig.salt = salt + } + switch sig.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly: sig.RSASignature = new(encoding.MPI) @@ -216,6 +300,16 @@ func (sig *Signature) parse(r io.Reader) (err error) { if _, err = sig.EdDSASigS.ReadFrom(r); err != nil { return } + case PubKeyAlgoEd25519: + sig.EdSig, err = ed25519.ReadSignature(r) + if err != nil { + return + } + case PubKeyAlgoEd448: + sig.EdSig, err = ed448.ReadSignature(r) + if err != nil { + return + } default: panic("unreachable") } @@ -260,6 +354,7 @@ const ( featuresSubpacket signatureSubpacketType = 30 embeddedSignatureSubpacket signatureSubpacketType = 32 issuerFingerprintSubpacket signatureSubpacketType = 33 + intendedRecipientSubpacket signatureSubpacketType = 35 prefCipherSuitesSubpacket signatureSubpacketType = 39 ) @@ -365,16 +460,18 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r copy(sig.PreferredSymmetric, subpacket) case issuerSubpacket: // Issuer, section 5.2.3.5 - if sig.Version > 4 { - err = errors.StructuralError("issuer subpacket found in v5 key") + if sig.Version > 4 && isHashed { + err = errors.StructuralError("issuer subpacket found in v6 key") return } if len(subpacket) != 8 { err = errors.StructuralError("issuer subpacket with bad length") return } - sig.IssuerKeyId = new(uint64) - *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket) + if sig.Version <= 4 { + sig.IssuerKeyId = new(uint64) + *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket) + } case notationDataSubpacket: // Notation data, section 5.2.3.16 if len(subpacket) < 8 { @@ -453,7 +550,7 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r return } sig.RevocationReason = new(ReasonForRevocation) - *sig.RevocationReason = ReasonForRevocation(subpacket[0]) + *sig.RevocationReason = NewReasonForRevocation(subpacket[0]) sig.RevocationReasonText = string(subpacket[1:]) case featuresSubpacket: // Features subpacket, section 5.2.3.24 specifies a very general @@ -495,17 +592,30 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r return } v, l := subpacket[0], len(subpacket[1:]) - if v == 5 && l != 32 || v != 5 && l != 20 { + if v >= 5 && l != 32 || v < 5 && l != 20 { return nil, errors.StructuralError("bad fingerprint length") } sig.IssuerFingerprint = make([]byte, l) copy(sig.IssuerFingerprint, subpacket[1:]) sig.IssuerKeyId = new(uint64) - if v == 5 { + if v >= 5 { *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket[1:9]) } else { *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket[13:21]) } + case intendedRecipientSubpacket: + // Intended Recipient Fingerprint + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-intended-recipient-fingerpr + if len(subpacket) < 1 { + return nil, errors.StructuralError("invalid intended recipient fingerpring length") + } + version, length := subpacket[0], len(subpacket[1:]) + if version >= 5 && length != 32 || version < 5 && length != 20 { + return nil, errors.StructuralError("invalid fingerprint length") + } + fingerprint := make([]byte, length) + copy(fingerprint, subpacket[1:]) + sig.IntendedRecipients = append(sig.IntendedRecipients, &Recipient{int(version), fingerprint}) case prefCipherSuitesSubpacket: // Preferred AEAD cipher suites // See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#name-preferred-aead-ciphersuites @@ -550,6 +660,13 @@ func (sig *Signature) CheckKeyIdOrFingerprint(pk *PublicKey) bool { return sig.IssuerKeyId != nil && *sig.IssuerKeyId == pk.KeyId } +func (sig *Signature) CheckKeyIdOrFingerprintExplicit(fingerprint []byte, keyId uint64) bool { + if sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) >= 20 && fingerprint != nil { + return bytes.Equal(sig.IssuerFingerprint, fingerprint) + } + return sig.IssuerKeyId != nil && *sig.IssuerKeyId == keyId +} + // serializeSubpacketLength marshals the given length into to. func serializeSubpacketLength(to []byte, length int) int { // RFC 4880, Section 4.2.2. @@ -598,20 +715,19 @@ func serializeSubpackets(to []byte, subpackets []outputSubpacket, hashed bool) { to = to[n:] } } - return } // SigExpired returns whether sig is a signature that has expired or is created // in the future. func (sig *Signature) SigExpired(currentTime time.Time) bool { - if sig.CreationTime.After(currentTime) { + if sig.CreationTime.Unix() > currentTime.Unix() { return true } if sig.SigLifetimeSecs == nil || *sig.SigLifetimeSecs == 0 { return false } expiry := sig.CreationTime.Add(time.Duration(*sig.SigLifetimeSecs) * time.Second) - return currentTime.After(expiry) + return currentTime.Unix() > expiry.Unix() } // buildHashSuffix constructs the HashSuffix member of sig in preparation for signing. @@ -635,20 +751,36 @@ func (sig *Signature) buildHashSuffix(hashedSubpackets []byte) (err error) { uint8(sig.SigType), uint8(sig.PubKeyAlgo), uint8(hashId), - uint8(len(hashedSubpackets) >> 8), - uint8(len(hashedSubpackets)), }) + hashedSubpacketsLength := len(hashedSubpackets) + if sig.Version == 6 { + // v6 signatures store the length in 4 octets + hashedFields.Write([]byte{ + uint8(hashedSubpacketsLength >> 24), + uint8(hashedSubpacketsLength >> 16), + uint8(hashedSubpacketsLength >> 8), + uint8(hashedSubpacketsLength), + }) + } else { + hashedFields.Write([]byte{ + uint8(hashedSubpacketsLength >> 8), + uint8(hashedSubpacketsLength), + }) + } + lenPrefix := hashedFields.Len() hashedFields.Write(hashedSubpackets) - var l uint64 = uint64(6 + len(hashedSubpackets)) + var l uint64 = uint64(lenPrefix + len(hashedSubpackets)) if sig.Version == 5 { + // v5 case hashedFields.Write([]byte{0x05, 0xff}) hashedFields.Write([]byte{ uint8(l >> 56), uint8(l >> 48), uint8(l >> 40), uint8(l >> 32), uint8(l >> 24), uint8(l >> 16), uint8(l >> 8), uint8(l), }) } else { - hashedFields.Write([]byte{0x04, 0xff}) + // v4 and v6 case + hashedFields.Write([]byte{byte(sig.Version), 0xff}) hashedFields.Write([]byte{ uint8(l >> 24), uint8(l >> 16), uint8(l >> 8), uint8(l), }) @@ -676,6 +808,67 @@ func (sig *Signature) signPrepareHash(h hash.Hash) (digest []byte, err error) { return } +// PrepareSign must be called to create a hash object before Sign for v6 signatures. +// The created hash object initially hashes a randomly generated salt +// as required by v6 signatures. The generated salt is stored in sig. If the signature is not v6, +// the method returns an empty hash object. +// See RFC the crypto refresh Section 3.2.4. +func (sig *Signature) PrepareSign(config *Config) (hash.Hash, error) { + if !sig.Hash.Available() { + return nil, errors.UnsupportedError("hash function") + } + hasher := sig.Hash.New() + if sig.Version == 6 { + if sig.salt == nil { + var err error + sig.salt, err = SignatureSaltForHash(sig.Hash, config.Random()) + if err != nil { + return nil, err + } + } + hasher.Write(sig.salt) + } + return hasher, nil +} + +// SetSalt sets the signature salt for v6 signatures. +// Assumes salt is generated correctly and checks if length matches. +// If the signature is not v6, the method ignores the salt. +// Use PrepareSign whenever possible instead of generating and +// hashing the salt externally. +// See RFC the crypto refresh Section 3.2.4. +func (sig *Signature) SetSalt(salt []byte) error { + if sig.Version == 6 { + expectedSaltLength, err := SaltLengthForHash(sig.Hash) + if err != nil { + return err + } + if salt == nil || len(salt) != expectedSaltLength { + return errors.InvalidArgumentError("unexpected salt size for the given hash algorithm") + } + sig.salt = salt + } + return nil +} + +// PrepareVerify must be called to create a hash object before verifying v6 signatures. +// The created hash object initially hashes the internally stored salt. +// If the signature is not v6, the method returns an empty hash object. +// See crypto refresh Section 3.2.4. +func (sig *Signature) PrepareVerify() (hash.Hash, error) { + if !sig.Hash.Available() { + return nil, errors.UnsupportedError("hash function") + } + hasher := sig.Hash.New() + if sig.Version == 6 { + if sig.salt == nil { + return nil, errors.StructuralError("v6 requires a salt for the hash to be signed") + } + hasher.Write(sig.salt) + } + return hasher, nil +} + // Sign signs a message with a private key. The hash, h, must contain // the hash of the message to be signed and will be mutated by this function. // On success, the signature is stored in sig. Call Serialize to write it out. @@ -729,6 +922,18 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e sig.EdDSASigR = encoding.NewMPI(r) sig.EdDSASigS = encoding.NewMPI(s) } + case PubKeyAlgoEd25519: + sk := priv.PrivateKey.(*ed25519.PrivateKey) + signature, err := ed25519.Sign(sk, digest) + if err == nil { + sig.EdSig = signature + } + case PubKeyAlgoEd448: + sk := priv.PrivateKey.(*ed448.PrivateKey) + signature, err := ed448.Sign(sk, digest) + if err == nil { + sig.EdSig = signature + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -744,11 +949,32 @@ func (sig *Signature) SignUserId(id string, pub *PublicKey, priv *PrivateKey, co if priv.Dummy() { return errors.ErrDummyPrivateKey("dummy key found") } - h, err := userIdSignatureHash(id, pub, sig.Hash) + prepareHash, err := sig.PrepareSign(config) if err != nil { return err } - return sig.Sign(h, priv, config) + if err := userIdSignatureHash(id, pub, prepareHash); err != nil { + return err + } + return sig.Sign(prepareHash, priv, config) +} + +// SignDirectKeyBinding computes a signature from priv +// On success, the signature is stored in sig. +// Call Serialize to write it out. +// If config is nil, sensible defaults will be used. +func (sig *Signature) SignDirectKeyBinding(pub *PublicKey, priv *PrivateKey, config *Config) error { + if priv.Dummy() { + return errors.ErrDummyPrivateKey("dummy key found") + } + prepareHash, err := sig.PrepareSign(config) + if err != nil { + return err + } + if err := directKeySignatureHash(pub, prepareHash); err != nil { + return err + } + return sig.Sign(prepareHash, priv, config) } // CrossSignKey computes a signature from signingKey on pub hashed using hashKey. On success, @@ -756,7 +982,11 @@ func (sig *Signature) SignUserId(id string, pub *PublicKey, priv *PrivateKey, co // If config is nil, sensible defaults will be used. func (sig *Signature) CrossSignKey(pub *PublicKey, hashKey *PublicKey, signingKey *PrivateKey, config *Config) error { - h, err := keySignatureHash(hashKey, pub, sig.Hash) + prepareHash, err := sig.PrepareSign(config) + if err != nil { + return err + } + h, err := keySignatureHash(hashKey, pub, prepareHash) if err != nil { return err } @@ -770,7 +1000,11 @@ func (sig *Signature) SignKey(pub *PublicKey, priv *PrivateKey, config *Config) if priv.Dummy() { return errors.ErrDummyPrivateKey("dummy key found") } - h, err := keySignatureHash(&priv.PublicKey, pub, sig.Hash) + prepareHash, err := sig.PrepareSign(config) + if err != nil { + return err + } + h, err := keySignatureHash(&priv.PublicKey, pub, prepareHash) if err != nil { return err } @@ -781,11 +1015,14 @@ func (sig *Signature) SignKey(pub *PublicKey, priv *PrivateKey, config *Config) // stored in sig. Call Serialize to write it out. // If config is nil, sensible defaults will be used. func (sig *Signature) RevokeKey(pub *PublicKey, priv *PrivateKey, config *Config) error { - h, err := keyRevocationHash(pub, sig.Hash) + prepareHash, err := sig.PrepareSign(config) if err != nil { return err } - return sig.Sign(h, priv, config) + if err := keyRevocationHash(pub, prepareHash); err != nil { + return err + } + return sig.Sign(prepareHash, priv, config) } // RevokeSubkey computes a subkey revocation signature of pub using priv. @@ -802,7 +1039,7 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { if len(sig.outSubpackets) == 0 { sig.outSubpackets = sig.rawSubpackets } - if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -819,16 +1056,24 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { case PubKeyAlgoEdDSA: sigLength = int(sig.EdDSASigR.EncodedLength()) sigLength += int(sig.EdDSASigS.EncodedLength()) + case PubKeyAlgoEd25519: + sigLength = ed25519.SignatureSize + case PubKeyAlgoEd448: + sigLength = ed448.SignatureSize default: panic("impossible") } + hashedSubpacketsLen := subpacketsLength(sig.outSubpackets, true) unhashedSubpacketsLen := subpacketsLength(sig.outSubpackets, false) - length := len(sig.HashSuffix) - 6 /* trailer not included */ + + length := 4 + /* length of version|signature type|public-key algorithm|hash algorithm */ + 2 /* length of hashed subpackets */ + hashedSubpacketsLen + 2 /* length of unhashed subpackets */ + unhashedSubpacketsLen + 2 /* hash tag */ + sigLength - if sig.Version == 5 { - length -= 4 // eight-octet instead of four-octet big endian + if sig.Version == 6 { + length += 4 + /* the two length fields are four-octet instead of two */ + 1 + /* salt length */ + len(sig.salt) /* length salt */ } err = serializeHeader(w, packetTypeSignature, length) if err != nil { @@ -842,18 +1087,41 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { } func (sig *Signature) serializeBody(w io.Writer) (err error) { - hashedSubpacketsLen := uint16(uint16(sig.HashSuffix[4])<<8) | uint16(sig.HashSuffix[5]) - fields := sig.HashSuffix[:6+hashedSubpacketsLen] + var fields []byte + if sig.Version == 6 { + // v6 signatures use 4 octets for length + hashedSubpacketsLen := + uint32(uint32(sig.HashSuffix[4])<<24) | + uint32(uint32(sig.HashSuffix[5])<<16) | + uint32(uint32(sig.HashSuffix[6])<<8) | + uint32(sig.HashSuffix[7]) + fields = sig.HashSuffix[:8+hashedSubpacketsLen] + } else { + hashedSubpacketsLen := uint16(uint16(sig.HashSuffix[4])<<8) | + uint16(sig.HashSuffix[5]) + fields = sig.HashSuffix[:6+hashedSubpacketsLen] + + } _, err = w.Write(fields) if err != nil { return } unhashedSubpacketsLen := subpacketsLength(sig.outSubpackets, false) - unhashedSubpackets := make([]byte, 2+unhashedSubpacketsLen) - unhashedSubpackets[0] = byte(unhashedSubpacketsLen >> 8) - unhashedSubpackets[1] = byte(unhashedSubpacketsLen) - serializeSubpackets(unhashedSubpackets[2:], sig.outSubpackets, false) + var unhashedSubpackets []byte + if sig.Version == 6 { + unhashedSubpackets = make([]byte, 4+unhashedSubpacketsLen) + unhashedSubpackets[0] = byte(unhashedSubpacketsLen >> 24) + unhashedSubpackets[1] = byte(unhashedSubpacketsLen >> 16) + unhashedSubpackets[2] = byte(unhashedSubpacketsLen >> 8) + unhashedSubpackets[3] = byte(unhashedSubpacketsLen) + serializeSubpackets(unhashedSubpackets[4:], sig.outSubpackets, false) + } else { + unhashedSubpackets = make([]byte, 2+unhashedSubpacketsLen) + unhashedSubpackets[0] = byte(unhashedSubpacketsLen >> 8) + unhashedSubpackets[1] = byte(unhashedSubpacketsLen) + serializeSubpackets(unhashedSubpackets[2:], sig.outSubpackets, false) + } _, err = w.Write(unhashedSubpackets) if err != nil { @@ -864,6 +1132,18 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } + if sig.Version == 6 { + // write salt for v6 signatures + _, err = w.Write([]byte{uint8(len(sig.salt))}) + if err != nil { + return + } + _, err = w.Write(sig.salt) + if err != nil { + return + } + } + switch sig.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly: _, err = w.Write(sig.RSASignature.EncodedBytes()) @@ -882,6 +1162,10 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } _, err = w.Write(sig.EdDSASigS.EncodedBytes()) + case PubKeyAlgoEd25519: + err = ed25519.WriteSignature(w, sig.EdSig) + case PubKeyAlgoEd448: + err = ed448.WriteSignature(w, sig.EdSig) default: panic("impossible") } @@ -908,7 +1192,7 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp } if sig.IssuerFingerprint != nil { contents := append([]uint8{uint8(issuer.Version)}, sig.IssuerFingerprint...) - subpackets = append(subpackets, outputSubpacket{true, issuerFingerprintSubpacket, sig.Version == 5, contents}) + subpackets = append(subpackets, outputSubpacket{true, issuerFingerprintSubpacket, sig.Version >= 5, contents}) } if sig.SignerUserId != nil { subpackets = append(subpackets, outputSubpacket{true, signerUserIdSubpacket, false, []byte(*sig.SignerUserId)}) @@ -958,6 +1242,17 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp }) } + for _, recipient := range sig.IntendedRecipients { + subpackets = append( + subpackets, + outputSubpacket{ + true, + intendedRecipientSubpacket, + false, + recipient.Serialize(), + }) + } + // The following subpackets may only appear in self-signatures. var features = byte(0x00) @@ -1073,8 +1368,6 @@ func (sig *Signature) AddMetadataToHashSuffix() { binary.BigEndian.PutUint32(buf[:], lit.Time) suffix.Write(buf[:]) - // Update the counter and restore trailing bytes - l = uint64(suffix.Len()) suffix.Write([]byte{0x05, 0xff}) suffix.Write([]byte{ uint8(l >> 56), uint8(l >> 48), uint8(l >> 40), uint8(l >> 32), @@ -1082,3 +1375,35 @@ func (sig *Signature) AddMetadataToHashSuffix() { }) sig.HashSuffix = suffix.Bytes() } + +// SaltLengthForHash selects the required salt length for the given hash algorithm, +// as per Table 23 (Hash algorithm registry) of the crypto refresh. +// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#section-9.5|Crypto Refresh Section 9.5. +func SaltLengthForHash(hash crypto.Hash) (int, error) { + switch hash { + case crypto.SHA256, crypto.SHA224, crypto.SHA3_256: + return 16, nil + case crypto.SHA384: + return 24, nil + case crypto.SHA512, crypto.SHA3_512: + return 32, nil + default: + return 0, errors.UnsupportedError("hash function not supported for V6 signatures") + } +} + +// SignatureSaltForHash generates a random signature salt +// with the length for the given hash algorithm. +// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#section-9.5|Crypto Refresh Section 9.5. +func SignatureSaltForHash(hash crypto.Hash, randReader io.Reader) ([]byte, error) { + saltLength, err := SaltLengthForHash(hash) + if err != nil { + return nil, err + } + salt := make([]byte, saltLength) + _, err = io.ReadFull(randReader, salt) + if err != nil { + return nil, err + } + return salt, nil +} diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index 669307301..20a5e17f7 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -315,7 +315,7 @@ func TestSignatureWithTrustAndRegex(t *testing.T) { } // ensure we fail if the regular expression is not null-terminated - packet, err = Read(readerFromHex(signatureWithBadTrustRegexHex)) + _, err = Read(readerFromHex(signatureWithBadTrustRegexHex)) if err == nil { t.Errorf("did not receive an error when expected") } diff --git a/openpgp/packet/symmetric_key_encrypted.go b/openpgp/packet/symmetric_key_encrypted.go index bac2b132e..c97b98b93 100644 --- a/openpgp/packet/symmetric_key_encrypted.go +++ b/openpgp/packet/symmetric_key_encrypted.go @@ -7,11 +7,13 @@ package packet import ( "bytes" "crypto/cipher" + "crypto/sha256" "io" "strconv" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "golang.org/x/crypto/hkdf" ) // This is the largest session key that we'll support. Since at most 256-bit cipher @@ -39,10 +41,17 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { return err } ske.Version = int(buf[0]) - if ske.Version != 4 && ske.Version != 5 { + if ske.Version != 4 && ske.Version != 5 && ske.Version != 6 { return errors.UnsupportedError("unknown SymmetricKeyEncrypted version") } + if ske.Version > 5 { + // Scalar octet count + if _, err := readFull(r, buf[:]); err != nil { + return err + } + } + // Cipher function if _, err := readFull(r, buf[:]); err != nil { return err @@ -52,7 +61,7 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[0]))) } - if ske.Version == 5 { + if ske.Version >= 5 { // AEAD mode if _, err := readFull(r, buf[:]); err != nil { return errors.StructuralError("cannot read AEAD octet from packet") @@ -60,6 +69,13 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { ske.Mode = AEADMode(buf[0]) } + if ske.Version > 5 { + // Scalar octet count + if _, err := readFull(r, buf[:]); err != nil { + return err + } + } + var err error if ske.s2k, err = s2k.Parse(r); err != nil { if _, ok := err.(errors.ErrDummyPrivateKey); ok { @@ -68,7 +84,7 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { return err } - if ske.Version == 5 { + if ske.Version >= 5 { // AEAD IV iv := make([]byte, ske.Mode.IvLength()) _, err := readFull(r, iv) @@ -109,8 +125,8 @@ func (ske *SymmetricKeyEncrypted) Decrypt(passphrase []byte) ([]byte, CipherFunc case 4: plaintextKey, cipherFunc, err := ske.decryptV4(key) return plaintextKey, cipherFunc, err - case 5: - plaintextKey, err := ske.decryptV5(key) + case 5, 6: + plaintextKey, err := ske.aeadDecrypt(ske.Version, key) return plaintextKey, CipherFunction(0), err } err := errors.UnsupportedError("unknown SymmetricKeyEncrypted version") @@ -136,9 +152,9 @@ func (ske *SymmetricKeyEncrypted) decryptV4(key []byte) ([]byte, CipherFunction, return plaintextKey, cipherFunc, nil } -func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) { - adata := []byte{0xc3, byte(5), byte(ske.CipherFunc), byte(ske.Mode)} - aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata) +func (ske *SymmetricKeyEncrypted) aeadDecrypt(version int, key []byte) ([]byte, error) { + adata := []byte{0xc3, byte(version), byte(ske.CipherFunc), byte(ske.Mode)} + aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata, version) plaintextKey, err := aead.Open(nil, ske.iv, ske.encryptedKey, adata) if err != nil { @@ -178,7 +194,7 @@ func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Conf func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, config *Config) (err error) { var version int if config.AEAD() != nil { - version = 5 + version = 6 } else { version = 4 } @@ -203,11 +219,15 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass switch version { case 4: packetLength = 2 /* header */ + len(s2kBytes) + 1 /* cipher type */ + keySize - case 5: + case 5, 6: ivLen := config.AEAD().Mode().IvLength() tagLen := config.AEAD().Mode().TagLength() packetLength = 3 + len(s2kBytes) + ivLen + keySize + tagLen } + if version > 5 { + packetLength += 2 // additional octet count fields + } + err = serializeHeader(w, packetTypeSymmetricKeyEncrypted, packetLength) if err != nil { return @@ -216,13 +236,22 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass // Symmetric Key Encrypted Version buf := []byte{byte(version)} + if version > 5 { + // Scalar octet count + buf = append(buf, byte(3+len(s2kBytes)+config.AEAD().Mode().IvLength())) + } + // Cipher function buf = append(buf, byte(cipherFunc)) - if version == 5 { + if version >= 5 { // AEAD mode buf = append(buf, byte(config.AEAD().Mode())) } + if version > 5 { + // Scalar octet count + buf = append(buf, byte(len(s2kBytes))) + } _, err = w.Write(buf) if err != nil { return @@ -243,10 +272,10 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass if err != nil { return } - case 5: + case 5, 6: mode := config.AEAD().Mode() - adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)} - aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata) + adata := []byte{0xc3, byte(version), byte(cipherFunc), byte(mode)} + aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata, version) // Sample iv using random reader iv := make([]byte, config.AEAD().Mode().IvLength()) @@ -270,7 +299,17 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass return } -func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte) (aead cipher.AEAD) { - blockCipher := c.new(inputKey) +func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte, version int) (aead cipher.AEAD) { + var blockCipher cipher.Block + if version > 5 { + hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData) + + encryptionKey := make([]byte, c.KeySize()) + _, _ = readFull(hkdfReader, encryptionKey) + + blockCipher = c.new(encryptionKey) + } else { + blockCipher = c.new(inputKey) + } return mode.new(blockCipher) } diff --git a/openpgp/packet/symmetric_key_encrypted_data_test.go b/openpgp/packet/symmetric_key_encrypted_data_test.go index 119405cb8..1d3249eb4 100644 --- a/openpgp/packet/symmetric_key_encrypted_data_test.go +++ b/openpgp/packet/symmetric_key_encrypted_data_test.go @@ -9,7 +9,7 @@ type packetSequence struct { contents string } -var keyAndIpePackets = []*packetSequence{aeadEaxRFC, aeadOcbRFC} +var keyAndIpePackets = []*packetSequence{symEncTestv6, aeadEaxRFC, aeadOcbRFC} // https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-complete-aead-eax-encrypted- var aeadEaxRFC = &packetSequence{ @@ -24,3 +24,10 @@ var aeadOcbRFC = &packetSequence{ packets: "c33d05070203089f0b7da3e5ea64779099e326e5400a90936cefb4e8eba08c6773716d1f2714540a38fcac529949dac529d3de31e15b4aeb729e330033dbedd4490107020e5ed2bc1e470abe8f1d644c7a6c8a567b0f7701196611a154ba9c2574cd056284a8ef68035c623d93cc708a43211bb6eaf2b27f7c18d571bcd83b20add3a08b73af15b9a098", contents: "cb1462000000000048656c6c6f2c20776f726c64210a", } + +// OpenPGP crypto refresh A.7.1. +var symEncTestv6 = &packetSequence{ + password: "password", + packets: "c33c061a07030b0308e9d39785b2070008ffb42e7c483ef4884457cb3726b9b3db9ff776e5f4d9a40952e2447298851abfff7526df2dd554417579a7799fd26902070306fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9ffc6fc6d65bbfd24dcd0790966e6d1e85a30053784cb1d8b6a0699ef12155a7b2ad6258531b57651fd7777912fa95e35d9b40216f69a4c248db28ff4331f1632907399e6ff9", + contents: "cb1362000000000048656c6c6f2c20776f726c6421d50e1ce2269a9eddef81032172b7ed7c", +} diff --git a/openpgp/packet/symmetric_key_encrypted_test.go b/openpgp/packet/symmetric_key_encrypted_test.go index 8294adb3a..bf7463bba 100644 --- a/openpgp/packet/symmetric_key_encrypted_test.go +++ b/openpgp/packet/symmetric_key_encrypted_test.go @@ -9,7 +9,6 @@ import ( "crypto/rand" "encoding/hex" "io" - "io/ioutil" mathrand "math/rand" "testing" @@ -42,11 +41,11 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) { } // Decrypt contents var edp EncryptedDataPacket - switch packet.(type) { + switch p := packet.(type) { case *SymmetricallyEncrypted: - edp, _ = packet.(*SymmetricallyEncrypted) + edp = p case *AEADEncrypted: - edp, _ = packet.(*AEADEncrypted) + edp = p default: t.Fatal("no integrity protected packet") } @@ -55,7 +54,7 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) { t.Fatal(err) } - contents, err := ioutil.ReadAll(r) + contents, err := io.ReadAll(r) if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { t.Fatal(err) } @@ -67,7 +66,7 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) { } } -func TestSerializeSymmetricKeyEncryptedV5RandomizeSlow(t *testing.T) { +func TestSerializeSymmetricKeyEncryptedV6RandomizeSlow(t *testing.T) { ciphers := map[string]CipherFunction{ "AES128": CipherAES128, "AES192": CipherAES192, @@ -102,6 +101,9 @@ func TestSerializeSymmetricKeyEncryptedV5RandomizeSlow(t *testing.T) { } key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) + if err != nil { + t.Errorf("failed to serialize %s", err) + } p, err := Read(&buf) if err != nil { t.Errorf("failed to reparse %s", err) @@ -151,7 +153,7 @@ func TestSerializeSymmetricKeyEncryptedCiphersV4(t *testing.T) { config := &Config{ DefaultCipher: cipher, S2KConfig: &s2k.Config{ - S2KMode: s2ktype, + S2KMode: s2ktype, PassphraseIsHighEntropy: true, }, } diff --git a/openpgp/packet/symmetrically_encrypted_aead.go b/openpgp/packet/symmetrically_encrypted_aead.go index e96252c19..a8ef0bbbe 100644 --- a/openpgp/packet/symmetrically_encrypted_aead.go +++ b/openpgp/packet/symmetrically_encrypted_aead.go @@ -115,7 +115,7 @@ func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite // Random salt salt := make([]byte, aeadSaltSize) - if _, err := rand.Read(salt); err != nil { + if _, err := io.ReadFull(rand, salt); err != nil { return nil, err } diff --git a/openpgp/packet/symmetrically_encrypted_mdc.go b/openpgp/packet/symmetrically_encrypted_mdc.go index fa26bebe3..645963fa7 100644 --- a/openpgp/packet/symmetrically_encrypted_mdc.go +++ b/openpgp/packet/symmetrically_encrypted_mdc.go @@ -237,7 +237,10 @@ func serializeSymmetricallyEncryptedMdc(ciphertext io.WriteCloser, c CipherFunct block := c.new(key) blockSize := block.BlockSize() iv := make([]byte, blockSize) - _, err = config.Random().Read(iv) + _, err = io.ReadFull(config.Random(), iv) + if err != nil { + return nil, err + } if err != nil { return } diff --git a/openpgp/packet/symmetrically_encrypted_test.go b/openpgp/packet/symmetrically_encrypted_test.go index ef63f8b00..21204b39d 100644 --- a/openpgp/packet/symmetrically_encrypted_test.go +++ b/openpgp/packet/symmetrically_encrypted_test.go @@ -11,7 +11,6 @@ import ( "encoding/hex" goerrors "errors" "io" - "io/ioutil" "testing" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -49,7 +48,7 @@ func TestMDCReader(t *testing.T) { for stride := 1; stride < len(mdcPlaintext)/2; stride++ { r := &testReader{data: mdcPlaintext, stride: stride} mdcReader := &seMDCReader{in: r, h: sha1.New()} - body, err := ioutil.ReadAll(mdcReader) + body, err := io.ReadAll(mdcReader) if err != nil { t.Errorf("stride: %d, error: %s", stride, err) continue @@ -69,7 +68,7 @@ func TestMDCReader(t *testing.T) { r := &testReader{data: mdcPlaintext, stride: 2} mdcReader := &seMDCReader{in: r, h: sha1.New()} - _, err := ioutil.ReadAll(mdcReader) + _, err := io.ReadAll(mdcReader) if err != nil { t.Errorf("corruption test, error: %s", err) return @@ -100,7 +99,10 @@ func TestSerializeMdc(t *testing.T) { contents := []byte("hello world\n") - w.Write(contents) + if _, err := w.Write(contents); err != nil { + t.Error(err) + return + } w.Close() p, err := Read(buf) @@ -197,7 +199,7 @@ func TestAeadRfcVector(t *testing.T) { return } - decrypted, err := ioutil.ReadAll(aeadReader) + decrypted, err := io.ReadAll(aeadReader) if err != nil { t.Errorf("error when reading: %s", err) return diff --git a/openpgp/packet/userattribute.go b/openpgp/packet/userattribute.go index 88ec72c6c..63814ed13 100644 --- a/openpgp/packet/userattribute.go +++ b/openpgp/packet/userattribute.go @@ -9,7 +9,6 @@ import ( "image" "image/jpeg" "io" - "io/ioutil" ) const UserAttrImageSubpacket = 1 @@ -63,7 +62,7 @@ func NewUserAttribute(contents ...*OpaqueSubpacket) *UserAttribute { func (uat *UserAttribute) parse(r io.Reader) (err error) { // RFC 4880, section 5.13 - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return } diff --git a/openpgp/packet/userid.go b/openpgp/packet/userid.go index 614fbafd5..3c7451a3c 100644 --- a/openpgp/packet/userid.go +++ b/openpgp/packet/userid.go @@ -6,7 +6,6 @@ package packet import ( "io" - "io/ioutil" "strings" ) @@ -66,7 +65,7 @@ func NewUserId(name, comment, email string) *UserId { func (uid *UserId) parse(r io.Reader) (err error) { // RFC 4880, section 5.11 - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return } diff --git a/openpgp/read.go b/openpgp/read.go index 8499c7379..ac897d709 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -6,6 +6,7 @@ package openpgp // import "github.com/ProtonMail/go-crypto/openpgp" import ( + "bytes" "crypto" _ "crypto/sha256" _ "crypto/sha512" @@ -46,6 +47,7 @@ type MessageDetails struct { DecryptedWith Key // the private key used to decrypt the message, if any. IsSigned bool // true if the message is signed. SignedByKeyId uint64 // the key id of the signer, if any. + SignedByFingerprint []byte // the key fingerprint of the signer, if any. SignedBy *Key // the key of the signer, if available. LiteralData *packet.LiteralData // the metadata of the contents UnverifiedBody io.Reader // the contents of the message. @@ -117,7 +119,7 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: break default: continue @@ -270,13 +272,17 @@ FindLiteralData: prevLast = true } - h, wrappedHash, err = hashForSignature(p.Hash, p.SigType) + h, wrappedHash, err = hashForSignature(p.Hash, p.SigType, p.Salt) if err != nil { md.SignatureError = err } md.IsSigned = true + if p.Version == 6 { + md.SignedByFingerprint = p.KeyFingerprint + } md.SignedByKeyId = p.KeyId + if keyring != nil { keys := keyring.KeysByIdUsage(p.KeyId, packet.KeyFlagSign) if len(keys) > 0 { @@ -292,7 +298,7 @@ FindLiteralData: if md.IsSigned && md.SignatureError == nil { md.UnverifiedBody = &signatureCheckReader{packets, h, wrappedHash, md, config} } else if md.decrypted != nil { - md.UnverifiedBody = checkReader{md} + md.UnverifiedBody = &checkReader{md, false} } else { md.UnverifiedBody = md.LiteralData.Body } @@ -300,12 +306,22 @@ FindLiteralData: return md, nil } +func wrapHashForSignature(hashFunc hash.Hash, sigType packet.SignatureType) (hash.Hash, error) { + switch sigType { + case packet.SigTypeBinary: + return hashFunc, nil + case packet.SigTypeText: + return NewCanonicalTextHash(hashFunc), nil + } + return nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType))) +} + // hashForSignature returns a pair of hashes that can be used to verify a // signature. The signature may specify that the contents of the signed message // should be preprocessed (i.e. to normalize line endings). Thus this function // returns two hashes. The second should be used to hash the message itself and // performs any needed preprocessing. -func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) { +func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType, sigSalt []byte) (hash.Hash, hash.Hash, error) { if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok { return nil, nil, errors.UnsupportedError("unsupported hash function") } @@ -313,14 +329,19 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash. return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc))) } h := hashFunc.New() - + if sigSalt != nil { + h.Write(sigSalt) + } + wrappedHash, err := wrapHashForSignature(h, sigType) + if err != nil { + return nil, nil, err + } switch sigType { case packet.SigTypeBinary: - return h, h, nil + return h, wrappedHash, nil case packet.SigTypeText: - return h, NewCanonicalTextHash(h), nil + return h, wrappedHash, nil } - return nil, nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType))) } @@ -328,16 +349,22 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash. // it closes the ReadCloser from any SymmetricallyEncrypted packet to trigger // MDC checks. type checkReader struct { - md *MessageDetails + md *MessageDetails + checked bool } -func (cr checkReader) Read(buf []byte) (int, error) { +func (cr *checkReader) Read(buf []byte) (int, error) { n, sensitiveParsingError := cr.md.LiteralData.Body.Read(buf) if sensitiveParsingError == io.EOF { + if cr.checked { + // Only check once + return n, io.EOF + } mdcErr := cr.md.decrypted.Close() if mdcErr != nil { return n, mdcErr } + cr.checked = true return n, io.EOF } @@ -428,14 +455,19 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) { // if any, and a possible signature verification error. // If the signer isn't known, ErrUnknownIssuer is returned. func VerifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { - var expectedHashes []crypto.Hash - return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) + return verifyDetachedSignature(keyring, signed, signature, nil, nil, false, config) } // VerifyDetachedSignatureAndHash performs the same actions as // VerifyDetachedSignature and checks that the expected hash functions were used. func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { - return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) + return verifyDetachedSignature(keyring, signed, signature, expectedHashes, nil, true, config) +} + +// VerifyDetachedSignatureAndSaltedHash performs the same actions as +// VerifyDetachedSignature and checks that the expected hash functions and salts were used. +func VerifyDetachedSignatureAndSaltedHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, expectedSaltedHashes []*packet.SaltedHashSpecifier, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { + return verifyDetachedSignature(keyring, signed, signature, expectedHashes, expectedSaltedHashes, true, config) } // CheckDetachedSignature takes a signed file and a detached signature and @@ -443,25 +475,31 @@ func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader // signature verification error. If the signer isn't known, // ErrUnknownIssuer is returned. func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (signer *Entity, err error) { - var expectedHashes []crypto.Hash - return CheckDetachedSignatureAndHash(keyring, signed, signature, expectedHashes, config) + _, signer, err = verifyDetachedSignature(keyring, signed, signature, nil, nil, false, config) + return +} + +// CheckDetachedSignatureAndSaltedHash performs the same actions as +// CheckDetachedSignature and checks that the expected hash functions or salted hash functions were used. +func CheckDetachedSignatureAndSaltedHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, expectedSaltedHashes []*packet.SaltedHashSpecifier, config *packet.Config) (signer *Entity, err error) { + _, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, expectedSaltedHashes, true, config) + return } // CheckDetachedSignatureAndHash performs the same actions as // CheckDetachedSignature and checks that the expected hash functions were used. func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (signer *Entity, err error) { - _, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) + _, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, nil, true, config) return } -func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { +func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, expectedSaltedHashes []*packet.SaltedHashSpecifier, checkHashes bool, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { var issuerKeyId uint64 var hashFunc crypto.Hash var sigType packet.SignatureType var keys []Key var p packet.Packet - expectedHashesLen := len(expectedHashes) packets := packet.NewReader(signature) for { p, err = packets.Next() @@ -483,16 +521,30 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec issuerKeyId = *sig.IssuerKeyId hashFunc = sig.Hash sigType = sig.SigType + if checkHashes { + matchFound := false + if sig.Version == 6 { + // check for salted hashes + for _, expectedSaltedHash := range expectedSaltedHashes { + if hashFunc == expectedSaltedHash.Hash && bytes.Equal(sig.Salt(), expectedSaltedHash.Salt) { + matchFound = true + break + } + } - for i, expectedHash := range expectedHashes { - if hashFunc == expectedHash { - break + } else { + // check for hashes + for _, expectedHash := range expectedHashes { + if hashFunc == expectedHash { + matchFound = true + break + } + } } - if i+1 == expectedHashesLen { - return nil, nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers") + if !matchFound { + return nil, nil, errors.StructuralError("hash algorithm or salt mismatch with cleartext message headers") } } - keys = keyring.KeysByIdUsage(issuerKeyId, packet.KeyFlagSign) if len(keys) > 0 { break @@ -503,7 +555,11 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec panic("unreachable") } - h, wrappedHash, err := hashForSignature(hashFunc, sigType) + h, err := sig.PrepareVerify() + if err != nil { + return nil, nil, err + } + wrappedHash, err := wrapHashForSignature(h, sigType) if err != nil { return nil, nil, err } @@ -551,15 +607,11 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader, // NOTE: The order of these checks is important, as the caller may choose to // ignore ErrSignatureExpired or ErrKeyExpired errors, but should never // ignore any other errors. -// -// TODO: Also return an error if: -// - The primary key is expired according to a direct-key signature -// - (For V5 keys only:) The direct-key signature (exists and) is expired func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet.Config) error { now := config.Now() - primaryIdentity := key.Entity.PrimaryIdentity() + primarySelfSignature, primaryIdentity := key.Entity.PrimarySelfSignature() signedBySubKey := key.PublicKey != key.Entity.PrimaryKey - sigsToCheck := []*packet.Signature{signature, primaryIdentity.SelfSignature} + sigsToCheck := []*packet.Signature{signature, primarySelfSignature} if signedBySubKey { sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature) } @@ -572,10 +624,10 @@ func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet } if key.Entity.Revoked(now) || // primary key is revoked (signedBySubKey && key.Revoked(now)) || // subkey is revoked - primaryIdentity.Revoked(now) { // primary identity is revoked + (primaryIdentity != nil && primaryIdentity.Revoked(now)) { // primary identity is revoked for v4 return errors.ErrKeyRevoked } - if key.Entity.PrimaryKey.KeyExpired(primaryIdentity.SelfSignature, now) { // primary key is expired + if key.Entity.PrimaryKey.KeyExpired(primarySelfSignature, now) { // primary key is expired return errors.ErrKeyExpired } if signedBySubKey { diff --git a/openpgp/read_test.go b/openpgp/read_test.go index bffa1c538..da3184012 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -131,7 +131,7 @@ func checkSignedMessage(t *testing.T, signedHex, expected string) { t.Errorf("bad MessageDetails: %#v", md) } - contents, err := ioutil.ReadAll(md.UnverifiedBody) + contents, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("error reading UnverifiedBody: %s", err) } @@ -229,7 +229,7 @@ func TestSignedEncryptedMessage(t *testing.T) { t.Errorf("#%d: bad MessageDetails: %#v", i, md) } - contents, err := ioutil.ReadAll(md.UnverifiedBody) + contents, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("#%d: error reading UnverifiedBody: %s", i, err) } @@ -248,7 +248,7 @@ func TestSignedEncryptedMessage(t *testing.T) { t.Errorf("#%d: error serializing verified signature: %s", i, err) } - sigData, err := ioutil.ReadAll(&sig) + sigData, err := io.ReadAll(&sig) if err != nil { t.Errorf("#%d: error reading verified signature: %s", i, err) } @@ -267,7 +267,7 @@ func TestSignedEncryptedMessage(t *testing.T) { } } - sigData, err := ioutil.ReadAll(&sig) + sigData, err := io.ReadAll(&sig) if err != nil { t.Errorf("#%d: error reading unverified signature: %s", i, err) } @@ -289,7 +289,7 @@ func TestUnspecifiedRecipient(t *testing.T) { return } - contents, err := ioutil.ReadAll(md.UnverifiedBody) + contents, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("error reading UnverifiedBody: %s", err) } @@ -324,7 +324,7 @@ func TestSymmetricallyEncrypted(t *testing.T) { return } - contents, err := ioutil.ReadAll(md.UnverifiedBody) + contents, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("ReadAll: %s", err) } @@ -450,7 +450,7 @@ func TestSignatureUnknownNotation(t *testing.T) { t.Error(err) return } - _, err = ioutil.ReadAll(md.UnverifiedBody) + _, err = io.ReadAll(md.UnverifiedBody) if err != nil { t.Error(err) return @@ -481,7 +481,7 @@ func TestSignatureKnownNotation(t *testing.T) { t.Error(err) return } - _, err = ioutil.ReadAll(md.UnverifiedBody) + _, err = io.ReadAll(md.UnverifiedBody) if err != nil { t.Error(err) return @@ -568,7 +568,7 @@ func TestSignatureV3Message(t *testing.T) { return } - _, err = ioutil.ReadAll(md.UnverifiedBody) + _, err = io.ReadAll(md.UnverifiedBody) if err != nil { t.Error(err) return @@ -585,7 +585,54 @@ func TestSignatureV3Message(t *testing.T) { t.Errorf("Did not expect a signature V4 back") return } - return +} + +func TestReadV6Messages(t *testing.T) { + key, err := ReadArmoredKeyRing(strings.NewReader(v6PrivKey)) + if err != nil { + t.Error(err) + return + } + msgReader, err := armor.Decode(strings.NewReader(v6PrivKeyMsg)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(msgReader.Body, key, nil, nil) + if err != nil { + t.Error(err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if string(contents) != "Hello, world!" { + t.Errorf("decrypted message is wrong: %s", contents) + } + + msgReader, err = armor.Decode(strings.NewReader(v6PrivKeyInlineSignMsg)) + if err != nil { + t.Error(err) + return + } + md, err = ReadMessage(msgReader.Body, key, nil, nil) + if err != nil { + t.Error(err) + return + } + contents, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if md.SignatureError != nil { + t.Error("expected no signature error, got:", md.SignatureError) + } + if string(contents) != "Hello, world!" { + t.Errorf("inline message is wrong: %s", contents) + } } func TestSymmetricDecryptionArgon2(t *testing.T) { @@ -595,7 +642,7 @@ func TestSymmetricDecryptionArgon2(t *testing.T) { if err != nil { t.Fatal(err) } - armoredEncryptedMessage, err := ioutil.ReadAll(file) + armoredEncryptedMessage, err := io.ReadAll(file) if err != nil { t.Fatal(err) } @@ -615,12 +662,12 @@ func TestSymmetricDecryptionArgon2(t *testing.T) { t.Error(err) return } - contents, err := ioutil.ReadAll(md.UnverifiedBody) + contents, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("error reading UnverifiedBody: %s", err) } - if "Hello, world!" != string(contents) { + if string(contents) != "Hello, world!" { t.Fatal("Did not decrypt Argon message correctly") } } @@ -632,12 +679,15 @@ func TestAsymmestricAeadOcbOpenPGPjsCompressedMessage(t *testing.T) { t.Fatal(err) } el, err := ReadArmoredKeyRing(armored) + if err != nil { + t.Fatal(err) + } // Read ciphertext from file ciphertext, err := os.Open("test_data/aead-ocb-asym-message.asc") if err != nil { t.Fatal(err) } - armoredEncryptedMessage, err := ioutil.ReadAll(ciphertext) + armoredEncryptedMessage, err := io.ReadAll(ciphertext) if err != nil { t.Fatal(err) } @@ -654,7 +704,7 @@ func TestAsymmestricAeadOcbOpenPGPjsCompressedMessage(t *testing.T) { return } // Read contents - contents, err := ioutil.ReadAll(md.UnverifiedBody) + contents, err := io.ReadAll(md.UnverifiedBody) if err != nil && err != io.ErrUnexpectedEOF { t.Errorf("error reading UnverifiedBody: %s", err) } @@ -677,12 +727,15 @@ func TestSymmetricAeadEaxOpenPGPJsMessage(t *testing.T) { if err != nil { t.Fatal(err) } - fileBytes, err := ioutil.ReadAll(file) + fileBytes, err := io.ReadAll(file) if err != nil { t.Fatal(err) } // Decode from base 64 raw, err := base64.StdEncoding.DecodeString(string(fileBytes)) + if err != nil { + t.Fatal(err) + } r := bytes.NewBuffer(raw) // Read packet p, err := packet.Read(r) @@ -691,18 +744,20 @@ func TestSymmetricAeadEaxOpenPGPJsMessage(t *testing.T) { } // Decrypt with key - var edp packet.EncryptedDataPacket - edp = p.(*packet.AEADEncrypted) + var edp = p.(*packet.AEADEncrypted) rc, err := edp.Decrypt(packet.CipherFunction(0), key) if err != nil { panic(err) } // Read literal data packet p, err = packet.Read(rc) + if err != nil { + t.Fatal(err) + } ld := p.(*packet.LiteralData) // Read contents - contents, err := ioutil.ReadAll(ld.Body) + contents, err := io.ReadAll(ld.Body) if err != nil && err != io.ErrUnexpectedEOF { t.Errorf("error reading UnverifiedBody: %s", err) } @@ -724,7 +779,7 @@ func TestCorruptedMessageInvalidSigHeader(t *testing.T) { if err != nil { t.Fatal(err) } - armoredEncryptedMessage, err := ioutil.ReadAll(file) + armoredEncryptedMessage, err := io.ReadAll(file) if err != nil { t.Fatal(err) } @@ -758,7 +813,7 @@ func TestCorruptedMessageWrongLength(t *testing.T) { if err != nil { t.Fatal(err) } - armoredEncryptedMessage, err := ioutil.ReadAll(file) + armoredEncryptedMessage, err := io.ReadAll(file) if err != nil { t.Fatal(err) } @@ -772,7 +827,7 @@ func TestCorruptedMessageWrongLength(t *testing.T) { t.Error(err) return } - _, err = ioutil.ReadAll(md.UnverifiedBody) + _, err = io.ReadAll(md.UnverifiedBody) if err == nil { t.Fatal("Parsing error expected") } @@ -823,7 +878,7 @@ func TestMessageWithoutMdc(t *testing.T) { t.Fatal("reading the message should have worked") } - b, err := ioutil.ReadAll(md.UnverifiedBody) + b, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Fatal("reading the message should have worked") } @@ -833,3 +888,38 @@ func TestMessageWithoutMdc(t *testing.T) { } }) } + +func TestReadV5Messages(t *testing.T) { + key, err := ReadArmoredKeyRing(strings.NewReader(keyv5Test)) + if err != nil { + t.Error(err) + return + } + keyVer, err := ReadArmoredKeyRing(strings.NewReader(certv5Test)) + if err != nil { + t.Error(err) + return + } + keys := append(key, keyVer...) + msgReader, err := armor.Decode(strings.NewReader(msgv5Test)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(msgReader.Body, keys, nil, nil) + if err != nil { + t.Error(err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if string(contents) != "Hello World :)" { + t.Errorf("decrypted message is wrong: %s", contents) + } + if md.SignatureError != nil { + t.Error("expected no signature error, got:", md.SignatureError) + } +} diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index db6dad5c0..670d60226 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -26,6 +26,8 @@ const testKeys1And2PrivateHex = "9501d8044d3c5c10010400b1d13382944bd5aba23a43129 const dsaElGamalTestKeysHex = "9501e1044dfcb16a110400aa3e5c1a1f43dd28c2ffae8abf5cfce555ee874134d8ba0a0f7b868ce2214beddc74e5e1e21ded354a95d18acdaf69e5e342371a71fbb9093162e0c5f3427de413a7f2c157d83f5cd2f9d791256dc4f6f0e13f13c3302af27f2384075ab3021dff7a050e14854bbde0a1094174855fc02f0bae8e00a340d94a1f22b32e48485700a0cec672ac21258fb95f61de2ce1af74b2c4fa3e6703ff698edc9be22c02ae4d916e4fa223f819d46582c0516235848a77b577ea49018dcd5e9e15cff9dbb4663a1ae6dd7580fa40946d40c05f72814b0f88481207e6c0832c3bded4853ebba0a7e3bd8e8c66df33d5a537cd4acf946d1080e7a3dcea679cb2b11a72a33a2b6a9dc85f466ad2ddf4c3db6283fa645343286971e3dd700703fc0c4e290d45767f370831a90187e74e9972aae5bff488eeff7d620af0362bfb95c1a6c3413ab5d15a2e4139e5d07a54d72583914661ed6a87cce810be28a0aa8879a2dd39e52fb6fe800f4f181ac7e328f740cde3d09a05cecf9483e4cca4253e60d4429ffd679d9996a520012aad119878c941e3cf151459873bdfc2a9563472fe0303027a728f9feb3b864260a1babe83925ce794710cfd642ee4ae0e5b9d74cee49e9c67b6cd0ea5dfbb582132195a121356a1513e1bca73e5b80c58c7ccb4164453412f456c47616d616c2054657374204b65792031886204131102002205024dfcb16a021b03060b090807030206150802090a0b0416020301021e01021780000a091033af447ccd759b09fadd00a0b8fd6f5a790bad7e9f2dbb7632046dc4493588db009c087c6a9ba9f7f49fab221587a74788c00db4889ab00200009d0157044dfcb16a1004008dec3f9291205255ccff8c532318133a6840739dd68b03ba942676f9038612071447bf07d00d559c5c0875724ea16a4c774f80d8338b55fca691a0522e530e604215b467bbc9ccfd483a1da99d7bc2648b4318fdbd27766fc8bfad3fddb37c62b8ae7ccfe9577e9b8d1e77c1d417ed2c2ef02d52f4da11600d85d3229607943700030503ff506c94c87c8cab778e963b76cf63770f0a79bf48fb49d3b4e52234620fc9f7657f9f8d56c96a2b7c7826ae6b57ebb2221a3fe154b03b6637cea7e6d98e3e45d87cf8dc432f723d3d71f89c5192ac8d7290684d2c25ce55846a80c9a7823f6acd9bb29fa6cd71f20bc90eccfca20451d0c976e460e672b000df49466408d527affe0303027a728f9feb3b864260abd761730327bca2aaa4ea0525c175e92bf240682a0e83b226f97ecb2e935b62c9a133858ce31b271fa8eb41f6a1b3cd72a63025ce1a75ee4180dcc284884904181102000905024dfcb16a021b0c000a091033af447ccd759b09dd0b009e3c3e7296092c81bee5a19929462caaf2fff3ae26009e218c437a2340e7ea628149af1ec98ec091a43992b00200009501e1044dfcb1be1104009f61faa61aa43df75d128cbe53de528c4aec49ce9360c992e70c77072ad5623de0a3a6212771b66b39a30dad6781799e92608316900518ec01184a85d872365b7d2ba4bacfb5882ea3c2473d3750dc6178cc1cf82147fb58caa28b28e9f12f6d1efcb0534abed644156c91cca4ab78834268495160b2400bc422beb37d237c2300a0cac94911b6d493bda1e1fbc6feeca7cb7421d34b03fe22cec6ccb39675bb7b94a335c2b7be888fd3906a1125f33301d8aa6ec6ee6878f46f73961c8d57a3e9544d8ef2a2cbfd4d52da665b1266928cfe4cb347a58c412815f3b2d2369dec04b41ac9a71cc9547426d5ab941cccf3b18575637ccfb42df1a802df3cfe0a999f9e7109331170e3a221991bf868543960f8c816c28097e503fe319db10fb98049f3a57d7c80c420da66d56f3644371631fad3f0ff4040a19a4fedc2d07727a1b27576f75a4d28c47d8246f27071e12d7a8de62aad216ddbae6aa02efd6b8a3e2818cda48526549791ab277e447b3a36c57cefe9b592f5eab73959743fcc8e83cbefec03a329b55018b53eec196765ae40ef9e20521a603c551efe0303020950d53a146bf9c66034d00c23130cce95576a2ff78016ca471276e8227fb30b1ffbd92e61804fb0c3eff9e30b1a826ee8f3e4730b4d86273ca977b4164453412f456c47616d616c2054657374204b65792032886204131102002205024dfcb1be021b03060b090807030206150802090a0b0416020301021e01021780000a0910a86bf526325b21b22bd9009e34511620415c974750a20df5cb56b182f3b48e6600a0a9466cb1a1305a84953445f77d461593f1d42bc1b00200009d0157044dfcb1be1004009565a951da1ee87119d600c077198f1c1bceb0f7aa54552489298e41ff788fa8f0d43a69871f0f6f77ebdfb14a4260cf9fbeb65d5844b4272a1904dd95136d06c3da745dc46327dd44a0f16f60135914368c8039a34033862261806bb2c5ce1152e2840254697872c85441ccb7321431d75a747a4bfb1d2c66362b51ce76311700030503fc0ea76601c196768070b7365a200e6ddb09307f262d5f39eec467b5f5784e22abdf1aa49226f59ab37cb49969d8f5230ea65caf56015abda62604544ed526c5c522bf92bed178a078789f6c807b6d34885688024a5bed9e9f8c58d11d4b82487b44c5f470c5606806a0443b79cadb45e0f897a561a53f724e5349b9267c75ca17fe0303020950d53a146bf9c660bc5f4ce8f072465e2d2466434320c1e712272fafc20e342fe7608101580fa1a1a367e60486a7cd1246b7ef5586cf5e10b32762b710a30144f12dd17dd4884904181102000905024dfcb1be021b0c000a0910a86bf526325b21b2904c00a0b2b66b4b39ccffda1d10f3ea8d58f827e30a8b8e009f4255b2d8112a184e40cde43a34e8655ca7809370b0020000" +const ed25519wX25519Key = "c54b0663877fe31b00000020f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3001972817b12be707e8d5f586ce61361201d344eb266a2c82fde6835762b65b0b7c2b1061f1b0a00000042058263877fe3030b090705150a0e080c021600029b03021e09222106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc905270902070200000000ad2820103e2d7d227ec0e6d7ce4471db36bfc97083253690271498a7ef0576c07faae14585b3b903b0127ec4fda2f023045a2ec76bcb4f9571a9651e14aee1137a1d668442c88f951e33c4ffd33fb9a17d511eed758fc6d9cc50cb5fd793b2039d5804c74b0663877fe319000000208693248367f9e5015db922f8f48095dda784987f2d5985b12fbad16caf5e4435004d600a4f794d44775c57a26e0feefed558e9afffd6ad0d582d57fb2ba2dcedb8c29b06181b0a0000002c050263877fe322a106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc9021b0c00000000defa20a6e9186d9d5935fc8fe56314cdb527486a5a5120f9b762a235a729f039010a56b89c658568341fbef3b894e9834ad9bc72afae2f4c9c47a43855e65f1cb0a3f77bbc5f61085c1f8249fe4e7ca59af5f0bcee9398e0fa8d76e522e1d8ab42bb0d" + const signedMessageHex = "a3019bc0cbccc0c4b8d8b74ee2108fe16ec6d3ca490cbe362d3f8333d3f352531472538b8b13d353b97232f352158c20943157c71c16064626063656269052062e4e01987e9b6fccff4b7df3a34c534b23e679cbec3bc0f8f6e64dfb4b55fe3f8efa9ce110ddb5cd79faf1d753c51aecfa669f7e7aa043436596cccc3359cb7dd6bbe9ecaa69e5989d9e57209571edc0b2fa7f57b9b79a64ee6e99ce1371395fee92fec2796f7b15a77c386ff668ee27f6d38f0baa6c438b561657377bf6acff3c5947befd7bf4c196252f1d6e5c524d0300" const signedTextMessageHex = "a3019bc0cbccc8c4b8d8b74ee2108fe16ec6d36a250cbece0c178233d3f352531472538b8b13d35379b97232f352158ca0b4312f57c71c1646462606365626906a062e4e019811591798ff99bf8afee860b0d8a8c2a85c3387e3bcf0bb3b17987f2bbcfab2aa526d930cbfd3d98757184df3995c9f3e7790e36e3e9779f06089d4c64e9e47dd6202cb6e9bc73c5d11bb59fbaf89d22d8dc7cf199ddf17af96e77c5f65f9bbed56f427bd8db7af37f6c9984bf9385efaf5f184f986fb3e6adb0ecfe35bbf92d16a7aa2a344fb0bc52fb7624f0200" @@ -160,18 +162,78 @@ TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw== =IiS2 -----END PGP PRIVATE KEY BLOCK-----` -// Generated with the above private key -const v5PrivKeyMsg = `-----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.10.7 -Comment: https://openpgpjs.org +// See OpenPGP crypto refresh Section A.3. +const v6PrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB +exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ +BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh +RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe +7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/ +LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG +GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE +M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr +k0mXubZvyl4GBg== +-----END PGP PRIVATE KEY BLOCK-----` + +// See OpenPGP crypto refresh merge request: +// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/304 +const v6PrivKeyMsg = `-----BEGIN PGP MESSAGE----- + +wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO +WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS +aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l +yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo +bhF30A+IitsxxA== +-----END PGP MESSAGE-----` + +// See OpenPGP crypto refresh merge request: +// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/305 +const v6PrivKeyInlineSignMsg = `-----BEGIN PGP MESSAGE----- -xA0DAQoWGTR7yYckZAIByxF1B21zZy50eHRfbIGSdGVzdMJ3BQEWCgAGBQJf -bIGSACMiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVDQvAP9G -y29VPonFXqi2zKkpZrvyvZxg+n5e8Nt9wNbuxeCd3QD/TtO2s+JvjrE4Siwv -UQdl5MlBka1QSNbMq2Bz7XwNPg4= -=6lbM +wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO +WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS +aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l +yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo +bhF30A+IitsxxA== -----END PGP MESSAGE-----` +// See https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/274 +// decryption password: "correct horse battery staple" +const v6ArgonSealedPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC +FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS +3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC +Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW +cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin +7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/ +0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0 +gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf +9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR +v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr +DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki +Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt +ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG +-----END PGP PRIVATE KEY BLOCK-----` + +const v4Key25519 = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUkEZB3qzRto01j2k2pwN5ux9w70stPinAdXULLr20CRW7U7h2GSeACch0M+ +qzQg8yjFQ8VBvu3uwgKH9senoHmj72lLSCLTmhFKzQR0ZXN0wogEEBsIAD4F +gmQd6s0ECwkHCAmQIf45+TuC+xMDFQgKBBYAAgECGQECmwMCHgEWIQSWEzMi +jJUHvyIbVKIh/jn5O4L7EwAAUhaHNlgudvxARdPPETUzVgjuWi+YIz8w1xIb +lHQMvIrbe2sGCQIethpWofd0x7DHuv/ciHg+EoxJ/Td6h4pWtIoKx0kEZB3q +zRm4CyA7quliq7yx08AoOqHTuuCgvpkSdEhpp3pEyejQOgBo0p6ywIiLPllY +0t+jpNspHpAGfXID6oqjpYuJw3AfVRBlwnQEGBsIACoFgmQd6s0JkCH+Ofk7 +gvsTApsMFiEElhMzIoyVB78iG1SiIf45+TuC+xMAAGgQuN9G73446ykvJ/mL +sCZ7zGFId2gBd1EnG0FTC4npfOKpck0X8dngByrCxU8LDSfvjsEp/xDAiKsQ +aU71tdtNBQ== +=e7jT +-----END PGP PRIVATE KEY BLOCK-----` + const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK----- xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv @@ -272,3 +334,124 @@ AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY hz3tYjKhoFTKEIq3y3Pp =h/aX -----END PGP PUBLIC KEY BLOCK-----` + +const keyv5Test = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +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 +=miES +-----END PGP PRIVATE KEY BLOCK----- +` + +const certv5Test = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +lGEFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd +fj75iux+my8QAAAAAAAiAQCHZ1SnSUmWqxEsoI6facIVZQu6mph3cBFzzTvcm5lA +Ng5ctBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0e8mHJGQC +X5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyICAQYVCgkI +CwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3wwJAXRJy9 +M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2BZxmBVyR9OQSAAAA +MgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0YvYWWAoD +AQgHAAAAAAAiAP9OdAPppjU1WwpqjIItkxr+VPQRT8Zm/Riw7U3F6v3OiBFHiHoF +GBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVAUCXJH05AIb +DAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijhob2U5AQC+RtOHCHx7 +TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw== +=IiS2 +-----END PGP PRIVATE KEY BLOCK----- +` + +const msgv5Test = `-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv+PcQiLsoYTH30nJYQh3j3cJaO2+jErtVCrIQRIU0+ +rmgMddERYST4A9mA0DQIiTI4FQ0Lp440D3BWCgpq3LlNWewGzduaWwym5rN6 +cwHz5ccDqOcqbd9X0GXXGy/ZH/ljSgzuVMIytMAXKdF/vrRrVgH/+I7cxvm9 +HwnhjMN5dF0j4aEt996H2T7cbtzSr2GN9SWGW8Gyu7I8Zx73hgrGUI7gDiJB +Afaff+P6hfkkHSGOItr94dde8J/7AUF4VEwwxdVVPvsNEFyvv6gRIbYtOCa2 +6RE6h1V/QTxW2O7zZgzWALrE2ui0oaYr9QuqQSssd9CdgExLfdPbI+3/ZAnE +v31Idzpk3/6ILiakYHtXkElPXvf46mCNpobty8ysT34irF+fy3C1p3oGwAsx +5VDV9OSFU6z5U+UPbSPYAy9rkc5ZssuIKxCER2oTvZ2L8Q5cfUvEUiJtRGGn +CJlHrVDdp3FssKv2tlKgLkvxJLyoOjuEkj44H1qRk+D02FzmmUT/0sAHAYYx +lTir6mjHeLpcGjn4waUuWIAJyph8SxUexP60bic0L0NBa6Qp5SxxijKsPIDb +FPHxWwfJSDZRrgUyYT7089YFB/ZM4FHyH9TZcnxn0f0xIB7NS6YNDsxzN2zT +EVEYf+De4qT/dQTsdww78Chtcv9JY9r2kDm77dk2MUGHL2j7n8jasbLtgA7h +pn2DMIWLrGamMLWRmlwslolKr1sMV5x8w+5Ias6C33iBMl9phkg42an0gYmc +byVJHvLO/XErtC+GNIJeMg== +=liRq +-----END PGP MESSAGE----- +` diff --git a/openpgp/s2k/s2k.go b/openpgp/s2k/s2k.go index a43695964..f4f5c7832 100644 --- a/openpgp/s2k/s2k.go +++ b/openpgp/s2k/s2k.go @@ -87,10 +87,10 @@ func decodeCount(c uint8) int { // encodeMemory converts the Argon2 "memory" in the range parallelism*8 to // 2**31, inclusive, to an encoded memory. The return value is the // octet that is actually stored in the GPG file. encodeMemory panics -// if is not in the above range +// if is not in the above range // See OpenPGP crypto refresh Section 3.7.1.4. func encodeMemory(memory uint32, parallelism uint8) uint8 { - if memory < (8 * uint32(parallelism)) || memory > uint32(2147483648) { + if memory < (8*uint32(parallelism)) || memory > uint32(2147483648) { panic("Memory argument memory is outside the required range") } @@ -211,7 +211,7 @@ func Generate(rand io.Reader, c *Config) (*Params, error) { c.S2KMode = IteratedSaltedS2K } params = &Params{ - mode: IteratedSaltedS2K, + mode: IteratedSaltedS2K, hashId: hashId, countByte: c.EncodedCount(), } @@ -306,9 +306,12 @@ func (params *Params) Dummy() bool { func (params *Params) salt() []byte { switch params.mode { - case SaltedS2K, IteratedSaltedS2K: return params.saltBytes[:8] - case Argon2S2K: return params.saltBytes[:Argon2SaltSize] - default: return nil + case SaltedS2K, IteratedSaltedS2K: + return params.saltBytes[:8] + case Argon2S2K: + return params.saltBytes[:Argon2SaltSize] + default: + return nil } } diff --git a/openpgp/s2k/s2k_cache.go b/openpgp/s2k/s2k_cache.go index 25a4442df..616e0d12c 100644 --- a/openpgp/s2k/s2k_cache.go +++ b/openpgp/s2k/s2k_cache.go @@ -5,7 +5,7 @@ package s2k // the same parameters. type Cache map[Params][]byte -// GetOrComputeDerivedKey tries to retrieve the key +// GetOrComputeDerivedKey tries to retrieve the key // for the given s2k parameters from the cache. // If there is no hit, it derives the key with the s2k function from the passphrase, // updates the cache, and returns the key. diff --git a/openpgp/s2k/s2k_config.go b/openpgp/s2k/s2k_config.go index b40be5228..b93db1ab8 100644 --- a/openpgp/s2k/s2k_config.go +++ b/openpgp/s2k/s2k_config.go @@ -50,9 +50,9 @@ type Config struct { type Argon2Config struct { NumberOfPasses uint8 DegreeOfParallelism uint8 - // The memory parameter for Argon2 specifies desired memory usage in kibibytes. + // Memory specifies the desired Argon2 memory usage in kibibytes. // For example memory=64*1024 sets the memory cost to ~64 MB. - Memory uint32 + Memory uint32 } func (c *Config) Mode() Mode { @@ -115,7 +115,7 @@ func (c *Argon2Config) EncodedMemory() uint8 { } memory := c.Memory - lowerBound := uint32(c.Parallelism())*8 + lowerBound := uint32(c.Parallelism()) * 8 upperBound := uint32(2147483648) switch { diff --git a/openpgp/s2k/s2k_test.go b/openpgp/s2k/s2k_test.go index f89b9694b..8a0720574 100644 --- a/openpgp/s2k/s2k_test.go +++ b/openpgp/s2k/s2k_test.go @@ -44,11 +44,11 @@ func TestSalted(t *testing.T) { } var argon2EncodeTest = []struct { - in uint32 + in uint32 out uint8 }{ - {64*1024, 16}, - {64*1024+1, 17}, + {64 * 1024, 16}, + {64*1024 + 1, 17}, {2147483647, 31}, {2147483649, 31}, {1, 3}, @@ -57,8 +57,8 @@ var argon2EncodeTest = []struct { func TestArgon2EncodeTest(t *testing.T) { for i, tests := range argon2EncodeTest { - conf := &Argon2Config { - Memory: tests.in, + conf := &Argon2Config{ + Memory: tests.in, DegreeOfParallelism: 1, } out := conf.EncodedMemory() @@ -68,7 +68,6 @@ func TestArgon2EncodeTest(t *testing.T) { } } - var iteratedTests = []struct { in, out string }{ @@ -240,8 +239,8 @@ func TestSerializeSaltedIteratedOK(t *testing.T) { func TestSerializeOKArgon(t *testing.T) { config := &Config{ - S2KMode: Argon2S2K, - Argon2Config: &Argon2Config{NumberOfPasses: 3, DegreeOfParallelism: 4, Memory: 64*1024}, + S2KMode: Argon2S2K, + Argon2Config: &Argon2Config{NumberOfPasses: 3, DegreeOfParallelism: 4, Memory: 64 * 1024}, } params := testSerializeConfigOK(t, config) diff --git a/openpgp/v2/canonical_text.go b/openpgp/v2/canonical_text.go new file mode 100644 index 000000000..d4de39ab4 --- /dev/null +++ b/openpgp/v2/canonical_text.go @@ -0,0 +1,117 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + "bytes" + "hash" + "io" +) + +// NewCanonicalTextHash reformats text written to it into the canonical +// form and then applies the hash h. See RFC 4880, section 5.2.1. +func NewCanonicalTextHash(h hash.Hash) hash.Hash { + return &canonicalTextHash{h, 0} +} + +// NewCanonicalTextWriteCloser reformats text written to it into the canonical +// form. See RFC 4880, section 5.2.1. +func NewCanonicalTextWriteCloser(w io.WriteCloser) io.WriteCloser { + return &canonicalTextWriteCloser{w, 0} +} + +// NewCanonicalTextReader reformats text read from it into the canonical +// form. See RFC 4880, section 5.2.1. +func NewCanonicalTextReader(r io.Reader) io.Reader { + return &canonicalTextReader{r, bytes.NewBuffer(nil), 0} +} + +type canonicalTextHash struct { + h hash.Hash + s int +} + +var newline = []byte{'\r', '\n'} + +func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) { + start := 0 + for i, c := range buf { + switch *s { + case 0: + if c == '\r' { + *s = 1 + } else if c == '\n' { + if _, err := cw.Write(buf[start:i]); err != nil { + return 0, err + } + if _, err := cw.Write(newline); err != nil { + return 0, err + } + start = i + 1 + } + case 1: + *s = 0 + } + } + + if _, err := cw.Write(buf[start:]); err != nil { + return 0, err + } + return len(buf), nil +} + +func (cth *canonicalTextHash) Write(buf []byte) (int, error) { + return writeCanonical(cth.h, buf, &cth.s) +} + +func (cth *canonicalTextHash) Sum(in []byte) []byte { + return cth.h.Sum(in) +} + +func (cth *canonicalTextHash) Reset() { + cth.h.Reset() + cth.s = 0 +} + +func (cth *canonicalTextHash) Size() int { + return cth.h.Size() +} + +func (cth *canonicalTextHash) BlockSize() int { + return cth.h.BlockSize() +} + +type canonicalTextWriteCloser struct { + w io.WriteCloser + s int +} + +func (tw *canonicalTextWriteCloser) Write(buf []byte) (int, error) { + return writeCanonical(tw.w, buf, &tw.s) +} + +func (tw *canonicalTextWriteCloser) Close() error { + return tw.w.Close() +} + +type canonicalTextReader struct { + r io.Reader + buffer *bytes.Buffer + s int +} + +func (tr *canonicalTextReader) Read(buf []byte) (int, error) { + if tr.buffer.Len() > 0 { + return tr.buffer.Read(buf) + } + n, err := tr.r.Read(buf) + if err != nil { + return n, err + } + if _, err = writeCanonical(tr.buffer, buf[:n], &tr.s); err != nil { + return n, err + } + return tr.buffer.Read(buf) +} diff --git a/openpgp/v2/canonical_text_test.go b/openpgp/v2/canonical_text_test.go new file mode 100644 index 000000000..0491d7455 --- /dev/null +++ b/openpgp/v2/canonical_text_test.go @@ -0,0 +1,52 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + "bytes" + "testing" +) + +type recordingHash struct { + buf *bytes.Buffer +} + +func (r recordingHash) Write(b []byte) (n int, err error) { + return r.buf.Write(b) +} + +func (r recordingHash) Sum(in []byte) []byte { + return append(in, r.buf.Bytes()...) +} + +func (r recordingHash) Reset() { + panic("shouldn't be called") +} + +func (r recordingHash) Size() int { + panic("shouldn't be called") +} + +func (r recordingHash) BlockSize() int { + panic("shouldn't be called") +} + +func testCanonicalText(t *testing.T, input, expected string) { + r := recordingHash{bytes.NewBuffer(nil)} + c := NewCanonicalTextHash(r) + c.Write([]byte(input)) + result := c.Sum(nil) + if expected != string(result) { + t.Errorf("input: %x got: %x want: %x", input, result, expected) + } +} + +func TestCanonicalText(t *testing.T) { + testCanonicalText(t, "foo\n", "foo\r\n") + testCanonicalText(t, "foo", "foo") + testCanonicalText(t, "foo\r\n", "foo\r\n") + testCanonicalText(t, "foo\r\nbar", "foo\r\nbar") + testCanonicalText(t, "foo\r\nbar\n\n", "foo\r\nbar\r\n\r\n") +} diff --git a/openpgp/v2/hash.go b/openpgp/v2/hash.go new file mode 100644 index 000000000..6bcb64b9e --- /dev/null +++ b/openpgp/v2/hash.go @@ -0,0 +1,24 @@ +package v2 + +import ( + "crypto" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +// HashIdToHash returns a crypto.Hash which corresponds to the given OpenPGP +// hash id. +func HashIdToHash(id byte) (h crypto.Hash, ok bool) { + return algorithm.HashIdToHash(id) +} + +// HashIdToString returns the name of the hash function corresponding to the +// given OpenPGP hash id. +func HashIdToString(id byte) (name string, ok bool) { + return algorithm.HashIdToString(id) +} + +// HashToHashId returns an OpenPGP hash id which corresponds the given Hash. +func HashToHashId(h crypto.Hash) (id byte, ok bool) { + return algorithm.HashToHashId(h) +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go new file mode 100644 index 000000000..5537d4f84 --- /dev/null +++ b/openpgp/v2/key_generation.go @@ -0,0 +1,524 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + goerrors "errors" + "io" + "math/big" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/ed25519" + "github.com/ProtonMail/go-crypto/openpgp/ed448" + "github.com/ProtonMail/go-crypto/openpgp/eddsa" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/x25519" + "github.com/ProtonMail/go-crypto/openpgp/x448" +) + +type userIdData struct { + name, comment, email string +} + +type keyProperties struct { + primaryKey *packet.PrivateKey + creationTime time.Time + keyLifetimeSecs uint32 + hash crypto.Hash + cipher packet.CipherFunction + aead *packet.AEADConfig + compression packet.CompressionAlgo +} + +// NewEntityWithoutId returns an Entity that contains fresh keys for signing and +// encrypting pgp messages. The key is not associated with an identity. +// This is only allowed for v6 key generation. If v6 is not enabled, +// it will return an error. +// If config is nil, sensible defaults will be used. +func NewEntityWithoutId(config *packet.Config) (*Entity, error) { + return newEntity(nil, config) +} + +// NewEntity returns an Entity that contains fresh keys with a for signing and +// encrypting pgp messages. The key is associated with a +// single identity composed of the given full name, comment and email, any of +// which may be empty but must not contain any of "()<>\x00". +// If config is nil, sensible defaults will be used. +func NewEntity(name, comment, email string, config *packet.Config) (*Entity, error) { + return newEntity(&userIdData{name, comment, email}, config) +} + +func selectKeyProperties(creationTime time.Time, config *packet.Config, primary *packet.PrivateKey) *keyProperties { + return &keyProperties{ + primaryKey: primary, + creationTime: creationTime, + keyLifetimeSecs: config.KeyLifetime(), + hash: config.Hash(), + cipher: config.Cipher(), + aead: config.AEAD(), + compression: config.Compression(), + } +} + +func newEntity(uid *userIdData, config *packet.Config) (*Entity, error) { + if uid == nil && !config.V6() { + return nil, errors.InvalidArgumentError("user id has to be set for non-v6 keys") + } + creationTime := config.Now() + + // Generate a primary signing key + primaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, err + } + primary := packet.NewSignerPrivateKey(creationTime, primaryPrivRaw) + if config.V6() { + primary.UpgradeToV6() + } + + keyProperties := selectKeyProperties(creationTime, config, primary) + + e := &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + DirectSignatures: []*packet.VerifiableSignature{}, + } + + if config.V6() { + if err := e.AddDirectKeySignature(keyProperties, config); err != nil { + return nil, err + } + keyProperties = nil + } + + if uid != nil { + err = e.addUserId(*uid, config, keyProperties) + if err != nil { + return nil, err + } + } + + // NOTE: No key expiry here, but we will not return this subkey in EncryptionKey() + // if the primary/master key has expired. + err = e.addEncryptionSubkey(config, creationTime, 0) + if err != nil { + return nil, err + } + + return e, nil +} + +// AddUserId adds a user-id packet to the given entity. +func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error { + var keyProperties *keyProperties + if !config.V6() { + keyProperties = selectKeyProperties(config.Now(), config, t.PrivateKey) + } + return t.addUserId(userIdData{name, comment, email}, config, keyProperties) +} + +// AddDirectKeySignature adds a fresh direct key signature with the selected key-properties. +func (t *Entity) AddDirectKeySignature(selectedKeyProperties *keyProperties, config *packet.Config) error { + selfSignature := createSignaturePacket(&t.PrivateKey.PublicKey, packet.SigTypeDirectSignature, config) + err := writeKeyProperties(selfSignature, selectedKeyProperties) + if err != nil { + return err + } + err = selfSignature.SignDirectKeyBinding(&t.PrivateKey.PublicKey, t.PrivateKey, config) + if err != nil { + return err + } + t.DirectSignatures = append(t.DirectSignatures, packet.NewVerifiableSig(selfSignature)) + return nil +} + +func writeKeyProperties(selfSignature *packet.Signature, selectedKeyProperties *keyProperties) error { + selfSignature.CreationTime = selectedKeyProperties.creationTime + selfSignature.KeyLifetimeSecs = &selectedKeyProperties.keyLifetimeSecs + selfSignature.FlagsValid = true + selfSignature.FlagSign = true + selfSignature.FlagCertify = true + selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14 + selfSignature.SEIPDv2 = selectedKeyProperties.aead != nil + + // Set the PreferredHash for the SelfSignature from the packet.Config. + // If it is not the must-implement algorithm from rfc4880bis, append that. + hash, ok := algorithm.HashToHashId(selectedKeyProperties.hash) + if !ok { + return errors.UnsupportedError("unsupported preferred hash function") + } + + selfSignature.PreferredHash = []uint8{} + // Ensure that for signing algorithms with higher security level an + // appropriate a matching hash function is available. + acceptableHashes := acceptableHashesToWrite(&selectedKeyProperties.primaryKey.PublicKey) + var match bool + for _, acceptableHashes := range acceptableHashes { + if acceptableHashes == hash { + match = true + break + } + } + if !match && len(acceptableHashes) > 0 { + selfSignature.PreferredHash = []uint8{acceptableHashes[0]} + } + + selfSignature.PreferredHash = append(selfSignature.PreferredHash, hash) + if selectedKeyProperties.hash != crypto.SHA256 { + selfSignature.PreferredHash = append(selfSignature.PreferredHash, hashToHashId(crypto.SHA256)) + } + + // Likewise for DefaultCipher. + selfSignature.PreferredSymmetric = []uint8{uint8(selectedKeyProperties.cipher)} + if selectedKeyProperties.cipher != packet.CipherAES128 { + selfSignature.PreferredSymmetric = append(selfSignature.PreferredSymmetric, uint8(packet.CipherAES128)) + } + + // We set CompressionNone as the preferred compression algorithm because + // of compression side channel attacks, then append the configured + // DefaultCompressionAlgo if any is set (to signal support for cases + // where the application knows that using compression is safe). + selfSignature.PreferredCompression = []uint8{uint8(packet.CompressionNone)} + if selectedKeyProperties.compression != packet.CompressionNone { + selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(selectedKeyProperties.compression)) + } + + // And for DefaultMode. + modes := []uint8{uint8(selectedKeyProperties.aead.Mode())} + if selectedKeyProperties.aead.Mode() != packet.AEADModeOCB { + modes = append(modes, uint8(packet.AEADModeOCB)) + } + + // For preferred (AES256, GCM), we'll generate (AES256, GCM), (AES256, OCB), (AES128, GCM), (AES128, OCB) + for _, cipher := range selfSignature.PreferredSymmetric { + for _, mode := range modes { + selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode}) + } + } + return nil +} + +func (t *Entity) addUserId(userIdData userIdData, config *packet.Config, selectedKeyProperties *keyProperties) error { + uid := packet.NewUserId(userIdData.name, userIdData.comment, userIdData.email) + if uid == nil { + return errors.InvalidArgumentError("user id field contained invalid characters") + } + + if _, ok := t.Identities[uid.Id]; ok { + return errors.InvalidArgumentError("user id exist") + } + + primary := t.PrivateKey + isPrimaryId := len(t.Identities) == 0 + selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config) + if selectedKeyProperties != nil { + err := writeKeyProperties(selfSignature, selectedKeyProperties) + if err != nil { + return err + } + } + selfSignature.IsPrimaryId = &isPrimaryId + + // User ID binding signature + err := selfSignature.SignUserId(uid.Id, &primary.PublicKey, primary, config) + if err != nil { + return err + } + t.Identities[uid.Id] = &Identity{ + Primary: t, + Name: uid.Id, + UserId: uid, + SelfCertifications: []*packet.VerifiableSignature{packet.NewVerifiableSig(selfSignature)}, + } + return nil +} + +// AddSigningSubkey adds a signing keypair as a subkey to the Entity. +// If config is nil, sensible defaults will be used. +func (e *Entity) AddSigningSubkey(config *packet.Config) error { + creationTime := config.Now() + keyLifetimeSecs := config.KeyLifetime() + + subPrivRaw, err := newSigner(config) + if err != nil { + return err + } + sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw) + sub.IsSubkey = true + // Every subkey for a v6 primary key MUST be a v6 subkey. + if e.PrimaryKey.Version == 6 { + sub.UpgradeToV6() + } + + subkey := Subkey{ + PublicKey: &sub.PublicKey, + PrivateKey: sub, + } + sig := createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyBinding, config) + sig.CreationTime = creationTime + sig.KeyLifetimeSecs = &keyLifetimeSecs + sig.FlagsValid = true + sig.FlagSign = true + sig.EmbeddedSignature = createSignaturePacket(subkey.PublicKey, packet.SigTypePrimaryKeyBinding, config) + sig.EmbeddedSignature.CreationTime = creationTime + + err = sig.EmbeddedSignature.CrossSignKey(subkey.PublicKey, e.PrimaryKey, subkey.PrivateKey, config) + if err != nil { + return err + } + + err = sig.SignKey(subkey.PublicKey, e.PrivateKey, config) + if err != nil { + return err + } + + subkey.Bindings = []*packet.VerifiableSignature{packet.NewVerifiableSig(sig)} + subkey.Primary = e + + e.Subkeys = append(e.Subkeys, subkey) + return nil +} + +// AddEncryptionSubkey adds an encryption keypair as a subkey to the Entity. +// If config is nil, sensible defaults will be used. +func (e *Entity) AddEncryptionSubkey(config *packet.Config) error { + creationTime := config.Now() + keyLifetimeSecs := config.KeyLifetime() + return e.addEncryptionSubkey(config, creationTime, keyLifetimeSecs) +} + +func (e *Entity) addEncryptionSubkey(config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32) error { + subPrivRaw, err := newDecrypter(config) + if err != nil { + return err + } + sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw) + sub.IsSubkey = true + // Every subkey for a v6 primary key MUST be a v6 subkey. + if e.PrimaryKey.Version == 6 { + sub.UpgradeToV6() + } + + subkey := Subkey{ + PublicKey: &sub.PublicKey, + PrivateKey: sub, + } + sig := createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyBinding, config) + sig.CreationTime = creationTime + sig.KeyLifetimeSecs = &keyLifetimeSecs + sig.FlagsValid = true + sig.FlagEncryptStorage = true + sig.FlagEncryptCommunications = true + + err = sig.SignKey(subkey.PublicKey, e.PrivateKey, config) + if err != nil { + return err + } + + subkey.Bindings = []*packet.VerifiableSignature{packet.NewVerifiableSig(sig)} + + subkey.Primary = e + e.Subkeys = append(e.Subkeys, subkey) + return nil +} + +// newSigner generates a signing key. +func newSigner(config *packet.Config) (signer interface{}, err error) { + switch config.PublicKeyAlgorithm() { + case packet.PubKeyAlgoRSA: + bits := config.RSAModulusBits() + if bits < 1024 { + return nil, errors.InvalidArgumentError("bits must be >= 1024") + } + if config != nil && len(config.RSAPrimes) >= 2 { + primes := config.RSAPrimes[0:2] + config.RSAPrimes = config.RSAPrimes[2:] + return generateRSAKeyWithPrimes(config.Random(), 2, bits, primes) + } + return rsa.GenerateKey(config.Random(), bits) + case packet.PubKeyAlgoEdDSA: + if config.V6() { + // Implementations MUST NOT accept or generate v6 key material + // using the deprecated OIDs. + return nil, errors.InvalidArgumentError("EdDSALegacy cannot be used for v6 keys") + } + curve := ecc.FindEdDSAByGenName(string(config.CurveName())) + if curve == nil { + return nil, errors.InvalidArgumentError("unsupported curve") + } + + priv, err := eddsa.GenerateKey(config.Random(), curve) + if err != nil { + return nil, err + } + return priv, nil + case packet.PubKeyAlgoECDSA: + curve := ecc.FindECDSAByGenName(string(config.CurveName())) + if curve == nil { + return nil, errors.InvalidArgumentError("unsupported curve") + } + + priv, err := ecdsa.GenerateKey(config.Random(), curve) + if err != nil { + return nil, err + } + return priv, nil + case packet.PubKeyAlgoEd25519: + priv, err := ed25519.GenerateKey(config.Random()) + if err != nil { + return nil, err + } + return priv, nil + case packet.PubKeyAlgoEd448: + priv, err := ed448.GenerateKey(config.Random()) + if err != nil { + return nil, err + } + return priv, nil + default: + return nil, errors.InvalidArgumentError("unsupported public key algorithm") + } +} + +// newDecrypter generates an encryption/decryption key. +func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { + switch config.PublicKeyAlgorithm() { + case packet.PubKeyAlgoRSA: + bits := config.RSAModulusBits() + if bits < 1024 { + return nil, errors.InvalidArgumentError("bits must be >= 1024") + } + if config != nil && len(config.RSAPrimes) >= 2 { + primes := config.RSAPrimes[0:2] + config.RSAPrimes = config.RSAPrimes[2:] + return generateRSAKeyWithPrimes(config.Random(), 2, bits, primes) + } + return rsa.GenerateKey(config.Random(), bits) + case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA: + fallthrough // When passing EdDSA or ECDSA, we generate an ECDH subkey + case packet.PubKeyAlgoECDH: + if config.V6() && + (config.CurveName() == packet.Curve25519 || + config.CurveName() == packet.Curve448) { + // Implementations MUST NOT accept or generate v6 key material + // using the deprecated OIDs. + return nil, errors.InvalidArgumentError("ECDH with Curve25519/448 legacy cannot be used for v6 keys") + } + var kdf = ecdh.KDF{ + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + } + curve := ecc.FindECDHByGenName(string(config.CurveName())) + if curve == nil { + return nil, errors.InvalidArgumentError("unsupported curve") + } + return ecdh.GenerateKey(config.Random(), curve, kdf) + case packet.PubKeyAlgoEd25519, packet.PubKeyAlgoX25519: // When passing Ed25519, we generate an X25519 subkey + return x25519.GenerateKey(config.Random()) + case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey + return x448.GenerateKey(config.Random()) + default: + return nil, errors.InvalidArgumentError("unsupported public key algorithm") + } +} + +var bigOne = big.NewInt(1) + +// generateRSAKeyWithPrimes generates a multi-prime RSA keypair of the +// given bit size, using the given random source and prepopulated primes. +func generateRSAKeyWithPrimes(random io.Reader, nprimes int, bits int, prepopulatedPrimes []*big.Int) (*rsa.PrivateKey, error) { + priv := new(rsa.PrivateKey) + priv.E = 65537 + + if nprimes < 2 { + return nil, goerrors.New("generateRSAKeyWithPrimes: nprimes must be >= 2") + } + + if bits < 1024 { + return nil, goerrors.New("generateRSAKeyWithPrimes: bits must be >= 1024") + } + + primes := make([]*big.Int, nprimes) + +NextSetOfPrimes: + for { + todo := bits + // crypto/rand should set the top two bits in each prime. + // Thus each prime has the form + // p_i = 2^bitlen(p_i) × 0.11... (in base 2). + // And the product is: + // P = 2^todo × α + // where α is the product of nprimes numbers of the form 0.11... + // + // If α < 1/2 (which can happen for nprimes > 2), we need to + // shift todo to compensate for lost bits: the mean value of 0.11... + // is 7/8, so todo + shift - nprimes * log2(7/8) ~= bits - 1/2 + // will give good results. + if nprimes >= 7 { + todo += (nprimes - 2) / 5 + } + for i := 0; i < nprimes; i++ { + var err error + if len(prepopulatedPrimes) == 0 { + primes[i], err = rand.Prime(random, todo/(nprimes-i)) + if err != nil { + return nil, err + } + } else { + primes[i] = prepopulatedPrimes[0] + prepopulatedPrimes = prepopulatedPrimes[1:] + } + + todo -= primes[i].BitLen() + } + + // Make sure that primes is pairwise unequal. + for i, prime := range primes { + for j := 0; j < i; j++ { + if prime.Cmp(primes[j]) == 0 { + continue NextSetOfPrimes + } + } + } + + n := new(big.Int).Set(bigOne) + totient := new(big.Int).Set(bigOne) + pminus1 := new(big.Int) + for _, prime := range primes { + n.Mul(n, prime) + pminus1.Sub(prime, bigOne) + totient.Mul(totient, pminus1) + } + if n.BitLen() != bits { + // This should never happen for nprimes == 2 because + // crypto/rand should set the top two bits in each prime. + // For nprimes > 2 we hope it does not happen often. + continue NextSetOfPrimes + } + + priv.D = new(big.Int) + e := big.NewInt(int64(priv.E)) + ok := priv.D.ModInverse(e, totient) + + if ok != nil { + priv.Primes = primes + priv.N = n + break + } + } + + priv.Precompute() + return priv, nil +} diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go new file mode 100644 index 000000000..77e00375c --- /dev/null +++ b/openpgp/v2/keys.go @@ -0,0 +1,786 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + goerrors "errors" + "fmt" + "io" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// PublicKeyType is the armor type for a PGP public key. +var PublicKeyType = "PGP PUBLIC KEY BLOCK" + +// PrivateKeyType is the armor type for a PGP private key. +var PrivateKeyType = "PGP PRIVATE KEY BLOCK" + +// An Entity represents the components of an OpenPGP key: a primary public key +// (which must be a signing key), one or more identities claimed by that key, +// and zero or more subkeys, which may be encryption keys. +type Entity struct { + PrimaryKey *packet.PublicKey + PrivateKey *packet.PrivateKey + Identities map[string]*Identity // indexed by Identity.Name + Revocations []*packet.VerifiableSignature + DirectSignatures []*packet.VerifiableSignature // Direct-key self signature of the PrimaryKey (contains primary key properties in v6)} + Subkeys []Subkey +} + +// A Key identifies a specific public key in an Entity. This is either the +// Entity's primary key or a subkey. +type Key struct { + Entity *Entity + PrimarySelfSignature *packet.Signature // might be nil, if not verified + PublicKey *packet.PublicKey + PrivateKey *packet.PrivateKey + SelfSignature *packet.Signature // might be nil, if not verified +} + +// A KeyRing provides access to public and private keys. +type KeyRing interface { + // KeysById returns the set of keys that have the given key id. + // KeysById does not perform any signature validations and verification of the returned keys. + KeysById(id uint64) []Key + // EntitiesById returns the set of entities that contain a key with the given key id. + // EntitiesById does not perform any signature validations and verification of the returned keys. + EntitiesById(id uint64) []*Entity +} + +// PrimaryIdentity returns a valid non-revoked Identity while preferring +// identities marked as primary, or the latest-created identity, in that order. +// Returns an nil for both return values if there is no valid primary identity. +func (e *Entity) PrimaryIdentity(date time.Time) (*packet.Signature, *Identity) { + var primaryIdentityCandidates []*Identity + var primaryIdentityCandidatesSelfSigs []*packet.Signature + for _, identity := range e.Identities { + selfSig, err := identity.Verify(date) // identity must be valid at date + if err == nil { // verification is successful + primaryIdentityCandidates = append(primaryIdentityCandidates, identity) + primaryIdentityCandidatesSelfSigs = append(primaryIdentityCandidatesSelfSigs, selfSig) + } + } + if len(primaryIdentityCandidates) == 0 { + return nil, nil + } + primaryIdentity := -1 + for idx := range primaryIdentityCandidates { + if primaryIdentity == -1 || + shouldPreferIdentity(primaryIdentityCandidatesSelfSigs[primaryIdentity], + primaryIdentityCandidatesSelfSigs[idx]) { + primaryIdentity = idx + } + } + return primaryIdentityCandidatesSelfSigs[primaryIdentity], primaryIdentityCandidates[primaryIdentity] +} + +func shouldPreferIdentity(existingId, potentialNewId *packet.Signature) bool { + // Prefer identities that are marked as primary + if existingId.IsPrimaryId != nil && *existingId.IsPrimaryId && + !(potentialNewId.IsPrimaryId != nil && *potentialNewId.IsPrimaryId) { + return false + } + if !(existingId.IsPrimaryId != nil && *existingId.IsPrimaryId) && + potentialNewId.IsPrimaryId != nil && *potentialNewId.IsPrimaryId { + return true + } + // after that newer creation time + return potentialNewId.CreationTime.Unix() >= existingId.CreationTime.Unix() +} + +// EncryptionKey returns the best candidate Key for encrypting a message to the +// given Entity. +func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) { + // The primary key has to be valid at time now + primarySelfSignature, err := e.VerifyPrimaryKey(now) + if err != nil { // primary key is not valid + return Key{}, false + } + + if checkKeyRequirements(e.PrimaryKey, config) != nil { + // The primary key produces weak signatures + return Key{}, false + } + + // Iterate the keys to find the newest, unexpired one + candidateSubkey := -1 + var maxTime time.Time + var selectedSubkeySelfSig *packet.Signature + for i, subkey := range e.Subkeys { + subkeySelfSig, err := subkey.Verify(now) // subkey has to be valid at time now + if err == nil && + isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo) && + checkKeyRequirements(subkey.PublicKey, config) == nil && + (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) { + candidateSubkey = i + selectedSubkeySelfSig = subkeySelfSig + maxTime = subkeySelfSig.CreationTime + } + } + + if candidateSubkey != -1 { + subkey := &e.Subkeys[candidateSubkey] + return Key{ + Entity: subkey.Primary, + PrimarySelfSignature: primarySelfSignature, + PublicKey: subkey.PublicKey, + PrivateKey: subkey.PrivateKey, + SelfSignature: selectedSubkeySelfSig, + }, true + } + + // If we don't have any subkeys for encryption and the primary key + // is marked as OK to encrypt with, then we can use it. + if isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { + return Key{ + Entity: e, + PrimarySelfSignature: primarySelfSignature, + PublicKey: e.PrimaryKey, + PrivateKey: e.PrivateKey, + SelfSignature: primarySelfSignature, + }, true + } + + return Key{}, false +} + +// DecryptionKeys returns all keys that are available for decryption, matching the keyID when given +// If date is zero (i.e., date.IsZero() == true) the time checks are not performed, +// which should be proffered to decrypt older messages. +// If id is 0 all decryption keys are returned. +// This is useful to retrieve keys for session key decryption. +func (e *Entity) DecryptionKeys(id uint64, date time.Time) (keys []Key) { + primarySelfSignature, err := e.PrimarySelfSignature(date) + if err != nil { // primary key is not valid + return + } + for _, subkey := range e.Subkeys { + subkeySelfSig, err := subkey.LatestValidBindingSignature(date) + if err == nil && + isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo) && + (id == 0 || subkey.PublicKey.KeyId == id) { + keys = append(keys, Key{subkey.Primary, primarySelfSignature, subkey.PublicKey, subkey.PrivateKey, subkeySelfSig}) + } + } + if isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { + keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, primarySelfSignature}) + } + return +} + +// CertificationKey return the best candidate Key for certifying a key with this +// Entity. +func (e *Entity) CertificationKey(now time.Time, config *packet.Config) (Key, bool) { + return e.CertificationKeyById(now, 0, config) +} + +// CertificationKeyById return the Key for key certification with this +// Entity and keyID. +func (e *Entity) CertificationKeyById(now time.Time, id uint64, config *packet.Config) (Key, bool) { + key, err := e.signingKeyByIdUsage(now, id, packet.KeyFlagSign, config) + return key, err == nil +} + +// SigningKey return the best candidate Key for signing a message with this +// Entity. +func (e *Entity) SigningKey(now time.Time, config *packet.Config) (Key, bool) { + return e.SigningKeyById(now, 0, config) +} + +// SigningKeyById return the Key for signing a message with this +// Entity and keyID. +func (e *Entity) SigningKeyById(now time.Time, id uint64, config *packet.Config) (Key, bool) { + key, err := e.signingKeyByIdUsage(now, id, packet.KeyFlagSign, config) + return key, err == nil +} + +func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config *packet.Config) (Key, error) { + primarySelfSignature, err := e.VerifyPrimaryKey(now) + if err != nil { + return Key{}, err + } + + if err = checkKeyRequirements(e.PrimaryKey, config); err != nil { + // The primary key produces weak signatures + return Key{}, err + } + + // Iterate the keys to find the newest, unexpired one. + candidateSubkey := -1 + var maxTime time.Time + var selectedSubkeySelfSig *packet.Signature + for idx, subkey := range e.Subkeys { + subkeySelfSig, err := subkey.Verify(now) + if err == nil && + (flags&packet.KeyFlagCertify == 0 || isValidCertificationKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && + (flags&packet.KeyFlagSign == 0 || isValidSigningKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && + checkKeyRequirements(subkey.PublicKey, config) == nil && + (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) && + (id == 0 || subkey.PublicKey.KeyId == id) { + candidateSubkey = idx + maxTime = subkeySelfSig.CreationTime + selectedSubkeySelfSig = subkeySelfSig + } + } + + if candidateSubkey != -1 { + subkey := &e.Subkeys[candidateSubkey] + return Key{ + Entity: subkey.Primary, + PrimarySelfSignature: primarySelfSignature, + PublicKey: subkey.PublicKey, + PrivateKey: subkey.PrivateKey, + SelfSignature: selectedSubkeySelfSig, + }, nil + } + + // If we don't have any subkeys for signing and the primary key + // is marked as OK to sign with, then we can use it. + if (flags&packet.KeyFlagCertify == 0 || isValidCertificationKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo)) && + (flags&packet.KeyFlagSign == 0 || isValidSigningKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo)) && + (id == 0 || e.PrimaryKey.KeyId == id) { + return Key{ + Entity: e, + PrimarySelfSignature: primarySelfSignature, + PublicKey: e.PrimaryKey, + PrivateKey: e.PrivateKey, + SelfSignature: primarySelfSignature, + }, nil + } + + // No keys with a valid Signing Flag or no keys matched the id passed in + return Key{}, errors.StructuralError("no valid signing or verifying key found") +} + +// Revoked returns whether the entity has any direct key revocation signatures. +// Note that third-party revocation signatures are not supported. +// Note also that Identity and Subkey revocation should be checked separately. +func (e *Entity) Revoked(now time.Time) bool { + // Verify revocations first + for _, revocation := range e.Revocations { + if revocation.Valid == nil { + err := e.PrimaryKey.VerifyRevocationSignature(revocation.Packet) + valid := err == nil + revocation.Valid = &valid + } + if *revocation.Valid && + (revocation.Packet.RevocationReason == nil || + *revocation.Packet.RevocationReason == packet.Unknown || + *revocation.Packet.RevocationReason == packet.NoReason || + *revocation.Packet.RevocationReason == packet.KeyCompromised) { + // If the key is compromised, the key is considered revoked even before the revocation date. + return true + } + if *revocation.Valid && + !revocation.Packet.SigExpired(now) { + return true + } + } + return false +} + +// EncryptPrivateKeys encrypts all non-encrypted keys in the entity with the same key +// derived from the provided passphrase. Public keys and dummy keys are ignored, +// and don't cause an error to be returned. +func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) error { + var keysToEncrypt []*packet.PrivateKey + // Add entity private key to encrypt. + if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted { + keysToEncrypt = append(keysToEncrypt, e.PrivateKey) + } + + // Add subkeys to encrypt. + for _, sub := range e.Subkeys { + if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && !sub.PrivateKey.Encrypted { + keysToEncrypt = append(keysToEncrypt, sub.PrivateKey) + } + } + return packet.EncryptPrivateKeys(keysToEncrypt, passphrase, config) +} + +// DecryptPrivateKeys decrypts all encrypted keys in the entity with the given passphrase. +// Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored, +// and don't cause an error to be returned. +func (e *Entity) DecryptPrivateKeys(passphrase []byte) error { + var keysToDecrypt []*packet.PrivateKey + // Add entity private key to decrypt. + if e.PrivateKey != nil && !e.PrivateKey.Dummy() && e.PrivateKey.Encrypted { + keysToDecrypt = append(keysToDecrypt, e.PrivateKey) + } + + // Add subkeys to decrypt. + for _, sub := range e.Subkeys { + if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && sub.PrivateKey.Encrypted { + keysToDecrypt = append(keysToDecrypt, sub.PrivateKey) + } + } + return packet.DecryptPrivateKeys(keysToDecrypt, passphrase) +} + +// EntityList contains one or more Entities. +type EntityList []*Entity + +// KeysById returns the set of keys that have the given key id. +// KeysById does not perform any key validation, and the self-signature +// fields in the returned key structs are nil. +func (el EntityList) KeysById(id uint64) (keys []Key) { + for _, e := range el { + if id == 0 || e.PrimaryKey.KeyId == id { + keys = append(keys, Key{e, nil, e.PrimaryKey, e.PrivateKey, nil}) + } + + for _, subKey := range e.Subkeys { + if id == 0 || subKey.PublicKey.KeyId == id { + keys = append(keys, Key{subKey.Primary, nil, subKey.PublicKey, subKey.PrivateKey, nil}) + } + } + } + return +} + +// EntitiesById returns the entities that contain a key with the given key id. +func (el EntityList) EntitiesById(id uint64) (entities []*Entity) { + for _, e := range el { + if id == 0 || e.PrimaryKey.KeyId == id { + entities = append(entities, e) + continue + } + + for _, subKey := range e.Subkeys { + if id == 0 || subKey.PublicKey.KeyId == id { + entities = append(entities, e) + continue + } + } + } + return +} + +// ReadArmoredKeyRing reads one or more public/private keys from an armor keyring file. +func ReadArmoredKeyRing(r io.Reader) (EntityList, error) { + block, err := armor.Decode(r) + if err == io.EOF { + return nil, errors.InvalidArgumentError("no armored data found") + } + if err != nil { + return nil, err + } + if block.Type != PublicKeyType && block.Type != PrivateKeyType { + return nil, errors.InvalidArgumentError("expected public or private key block, got: " + block.Type) + } + + return ReadKeyRing(block.Body) +} + +// ReadKeyRing reads one or more public/private keys. Unsupported keys are +// ignored as long as at least a single valid key is found. +func ReadKeyRing(r io.Reader) (el EntityList, err error) { + packets := packet.NewReader(r) + var lastUnsupportedError error + + for { + var e *Entity + e, err = ReadEntity(packets) + if err != nil { + // TODO: warn about skipped unsupported/unreadable keys + if _, ok := err.(errors.UnsupportedError); ok { + lastUnsupportedError = err + err = readToNextPublicKey(packets) + } else if _, ok := err.(errors.StructuralError); ok { + // Skip unreadable, badly-formatted keys + lastUnsupportedError = err + err = readToNextPublicKey(packets) + } + if err == io.EOF { + err = nil + break + } + if err != nil { + el = nil + break + } + } else { + el = append(el, e) + } + } + + if len(el) == 0 && err == nil { + err = lastUnsupportedError + } + return +} + +// readToNextPublicKey reads packets until the start of the entity and leaves +// the first packet of the new entity in the Reader. +func readToNextPublicKey(packets *packet.Reader) (err error) { + var p packet.Packet + for { + p, err = packets.Next() + if err == io.EOF { + return + } else if err != nil { + if _, ok := err.(errors.UnsupportedError); ok { + continue + } + return + } + + if pk, ok := p.(*packet.PublicKey); ok && !pk.IsSubkey { + packets.Unread(p) + return + } + } +} + +// ReadEntity reads an entity (public key, identities, subkeys etc) from the +// given Reader. +func ReadEntity(packets *packet.Reader) (*Entity, error) { + e := new(Entity) + e.Identities = make(map[string]*Identity) + + p, err := packets.Next() + if err != nil { + return nil, err + } + + var ok bool + if e.PrimaryKey, ok = p.(*packet.PublicKey); !ok { + if e.PrivateKey, ok = p.(*packet.PrivateKey); !ok { + packets.Unread(p) + return nil, errors.StructuralError("first packet was not a public/private key") + } + e.PrimaryKey = &e.PrivateKey.PublicKey + } + + if !e.PrimaryKey.PubKeyAlgo.CanSign() { + return nil, errors.StructuralError("primary key cannot be used for signatures") + } + var ignoreSigs bool +EachPacket: + for { + p, err := packets.NextWithUnsupported() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + var unsupported bool + if unsupportedPacket, ok := p.(*packet.UnsupportedPacket); ok { + unsupported = true + p = unsupportedPacket.IncompletePacket + } + + // Handle unsupported keys + switch p.(type) { + case *packet.PublicKey, *packet.PrivateKey: + if unsupported { + // Skip following signature packets + ignoreSigs = true + } + case *packet.Signature: + if ignoreSigs { + continue + } + default: + ignoreSigs = false + } + // Unsupported packages are handled continue + // if the packet is unsupported + if unsupported { + continue + } + + switch pkt := p.(type) { + case *packet.UserId: + err := readUser(e, packets, pkt) + if err != nil { + return nil, err + } + case *packet.Signature: + if pkt.SigType == packet.SigTypeKeyRevocation { + e.Revocations = append(e.Revocations, packet.NewVerifiableSig(pkt)) + } else if pkt.SigType == packet.SigTypeDirectSignature { + e.DirectSignatures = append(e.DirectSignatures, packet.NewVerifiableSig(pkt)) + } + // Else, ignoring the signature as it does not follow anything + // we would know to attach it to. + case *packet.PrivateKey: + if !pkt.IsSubkey { + packets.Unread(p) + break EachPacket + } + err = readSubkey(e, packets, &pkt.PublicKey, pkt) + if err != nil { + return nil, err + } + case *packet.PublicKey: + if !pkt.IsSubkey { + packets.Unread(p) + break EachPacket + } + err = readSubkey(e, packets, pkt, nil) + if err != nil { + return nil, err + } + default: + // we ignore unknown packets + } + } + + if len(e.Identities) == 0 && e.PrimaryKey.Version < 6 { + return nil, errors.StructuralError("v4 entity without any identities") + } + + if e.PrimaryKey.Version == 6 && len(e.DirectSignatures) == 0 { + return nil, errors.StructuralError("v6 entity without a direct-key signature") + } + return e, nil +} + +// SerializePrivate serializes an Entity, including private key material, but +// excluding signatures from other entities, to the given Writer. +// Identities and subkeys are re-signed in case they changed since NewEntry. +// If config is nil, sensible defaults will be used. +func (e *Entity) SerializePrivate(w io.Writer, config *packet.Config) (err error) { + if e.PrivateKey.Dummy() { + return errors.ErrDummyPrivateKey("dummy private key cannot re-sign identities") + } + return e.serializePrivate(w, config, true) +} + +// SerializePrivateWithoutSigning serializes an Entity, including private key +// material, but excluding signatures from other entities, to the given Writer. +// Self-signatures of identities and subkeys are not re-signed. This is useful +// when serializing GNU dummy keys, among other things. +// If config is nil, sensible defaults will be used. +func (e *Entity) SerializePrivateWithoutSigning(w io.Writer, config *packet.Config) (err error) { + return e.serializePrivate(w, config, false) +} + +func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign bool) (err error) { + if e.PrivateKey == nil { + return goerrors.New("openpgp: private key is missing") + } + err = e.PrivateKey.Serialize(w) + if err != nil { + return + } + for _, revocation := range e.Revocations { + if err = revocation.Packet.Serialize(w); err != nil { + return err + } + } + for _, directSignature := range e.DirectSignatures { + if err = directSignature.Packet.Serialize(w); err != nil { + return err + } + } + for _, ident := range e.Identities { + if reSign { + if err = ident.ReSign(config); err != nil { + return err + } + } + if err = ident.Serialize(w); err != nil { + return err + } + } + for _, subkey := range e.Subkeys { + if reSign { + if err := subkey.ReSign(config); err != nil { + return err + } + } + if err = subkey.Serialize(w, true); err != nil { + return err + } + } + return nil +} + +// Serialize writes the public part of the given Entity to w, including +// signatures from other entities. No private key material will be output. +func (e *Entity) Serialize(w io.Writer) error { + if err := e.PrimaryKey.Serialize(w); err != nil { + return err + } + for _, revocation := range e.Revocations { + if err := revocation.Packet.Serialize(w); err != nil { + return err + } + } + for _, directSignature := range e.DirectSignatures { + err := directSignature.Packet.Serialize(w) + if err != nil { + return err + } + } + for _, ident := range e.Identities { + if err := ident.Serialize(w); err != nil { + return err + } + } + for _, subkey := range e.Subkeys { + if err := subkey.Serialize(w, false); err != nil { + return err + } + } + return nil +} + +// Revoke generates a key revocation signature (packet.SigTypeKeyRevocation) with the +// specified reason code and text (RFC4880 section-5.2.3.23). +// If config is nil, sensible defaults will be used. +func (e *Entity) Revoke(reason packet.ReasonForRevocation, reasonText string, config *packet.Config) error { + revSig := createSignaturePacket(e.PrimaryKey, packet.SigTypeKeyRevocation, config) + revSig.RevocationReason = &reason + revSig.RevocationReasonText = reasonText + + if err := revSig.RevokeKey(e.PrimaryKey, e.PrivateKey, config); err != nil { + return err + } + sig := packet.NewVerifiableSig(revSig) + valid := true + sig.Valid = &valid + e.Revocations = append(e.Revocations, sig) + return nil +} + +// SignIdentity adds a signature to e, from signer, attesting that identity is +// associated with e. The provided identity must already be an element of +// e.Identities and the private key of signer must have been decrypted if +// necessary. +// If config is nil, sensible defaults will be used. +func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Config) error { + ident, ok := e.Identities[identity] + if !ok { + return errors.InvalidArgumentError("given identity string not found in Entity") + } + return ident.SignIdentity(signer, config) +} + +// LatestValidDirectSignature returns the latest valid direct key-signature of the entity. +func (e *Entity) LatestValidDirectSignature(date time.Time) (selectedSig *packet.Signature, err error) { + for sigIdx := len(e.DirectSignatures) - 1; sigIdx >= 0; sigIdx-- { + sig := e.DirectSignatures[sigIdx] + if (date.IsZero() || date.Unix() >= sig.Packet.CreationTime.Unix()) && + (selectedSig == nil || selectedSig.CreationTime.Unix() < sig.Packet.CreationTime.Unix()) { + if sig.Valid == nil { + err := e.PrimaryKey.VerifyDirectKeySignature(sig.Packet) + valid := err == nil + sig.Valid = &valid + } + if *sig.Valid && (date.IsZero() || !sig.Packet.SigExpired(date)) { + selectedSig = sig.Packet + } + } + } + if selectedSig == nil { + return nil, errors.StructuralError("no valid direct key signature found") + } + return +} + +// PrimarySelfSignature searches the entity for the self-signature that stores key preferences. +// For V4 keys, returns the self-signature of the primary identity, and the identity. +// For V6 keys, returns the latest valid direct-key self-signature, and no identity (nil). +// This self-signature is to be used to check the key expiration, +// algorithm preferences, and so on. +func (e *Entity) PrimarySelfSignature(date time.Time) (primarySig *packet.Signature, err error) { + if e.PrimaryKey.Version == 6 { + primarySig, err = e.LatestValidDirectSignature(date) + return + } + primarySig, _ = e.PrimaryIdentity(date) + if primarySig == nil { + return nil, errors.StructuralError("no primary identity found") + } + return +} + +// VerifyPrimaryKey checks if the primary key is valid by checking: +// - that the primary key is has not been revoked at the given date, +// - that there is valid non-expired self-signature, +// - that the primary key is not expired given its self-signature. +// If date is zero (i.e., date.IsZero() == true) the time checks are not performed. +func (e *Entity) VerifyPrimaryKey(date time.Time) (*packet.Signature, error) { + primarySelfSignature, err := e.PrimarySelfSignature(date) + if err != nil { + return nil, goerrors.New("no valid self signature found") + } + // check for key revocation signatures + if e.Revoked(date) { + return nil, errors.ErrKeyRevoked + } + + if !date.IsZero() && (e.PrimaryKey.KeyExpired(primarySelfSignature, date) || // primary key has expired + primarySelfSignature.SigExpired(date)) { // self-signature has expired + return primarySelfSignature, errors.ErrKeyExpired + } + + if e.PrimaryKey.Version != 6 && len(e.DirectSignatures) > 0 { + // check for expiration time in direct signatures (for V6 keys, the above already did so) + primaryDirectKeySignature, _ := e.LatestValidDirectSignature(date) + if primaryDirectKeySignature != nil && + (!date.IsZero() && e.PrimaryKey.KeyExpired(primaryDirectKeySignature, date)) { + return primarySelfSignature, errors.ErrKeyExpired + } + } + return primarySelfSignature, nil +} + +func (k *Key) IsPrimary() bool { + if k.PrimarySelfSignature == nil || k.SelfSignature == nil { + return k.PublicKey == k.Entity.PrimaryKey + } + return k.PrimarySelfSignature == k.SelfSignature +} + +func checkKeyRequirements(usedKey *packet.PublicKey, config *packet.Config) error { + algo := usedKey.PubKeyAlgo + if config.RejectPublicKeyAlgorithm(algo) { + return errors.WeakAlgorithmError("public key algorithm " + string(algo)) + } + switch algo { + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSASignOnly: + length, err := usedKey.BitLength() + if err != nil || length < config.MinimumRSABits() { + return errors.WeakAlgorithmError(fmt.Sprintf("minimum rsa length is %d got %d", config.MinimumRSABits(), length)) + } + case packet.PubKeyAlgoECDH, packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA: + curve, err := usedKey.Curve() + if err != nil || config.RejectCurve(curve) { + return errors.WeakAlgorithmError("elliptic curve " + curve) + } + if usedKey.Version == 6 && (curve == packet.Curve25519 || curve == packet.Curve448) { + // Implementations MUST NOT accept or generate v6 key material using the deprecated OIDs. + return errors.StructuralError("v6 key uses legacy elliptic curve " + curve) + } + } + return nil +} + +func isValidSigningKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanSign() && + signature.FlagsValid && + signature.FlagSign +} + +func isValidCertificationKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanSign() && + signature.FlagsValid && + signature.FlagCertify +} + +func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanEncrypt() && + signature.FlagsValid && + signature.FlagEncryptCommunications +} diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go new file mode 100644 index 000000000..da6b234fd --- /dev/null +++ b/openpgp/v2/keys_test.go @@ -0,0 +1,2013 @@ +package v2 + +import ( + "bytes" + "crypto" + "crypto/dsa" + "crypto/rand" + "crypto/rsa" + "fmt" + "math/big" + "strconv" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/eddsa" + "github.com/ProtonMail/go-crypto/openpgp/elgamal" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" +) + +var hashes = []crypto.Hash{ + crypto.SHA1, + crypto.SHA224, + crypto.SHA256, + crypto.SHA384, + crypto.SHA512, + crypto.SHA3_256, + crypto.SHA3_512, +} + +var ciphers = []packet.CipherFunction{ + packet.Cipher3DES, + packet.CipherCAST5, + packet.CipherAES128, + packet.CipherAES192, + packet.CipherAES256, +} + +var aeadModes = []packet.AEADMode{ + packet.AEADModeOCB, + packet.AEADModeEAX, + packet.AEADModeGCM, +} + +var allowAllAlgorithmsConfig = packet.Config{ + RejectPublicKeyAlgorithms: map[packet.PublicKeyAlgorithm]bool{}, + RejectCurves: map[packet.Curve]bool{}, + RejectMessageHashAlgorithms: map[crypto.Hash]bool{}, + MinRSABits: 512, +} + +func TestKeyExpiry(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(expiringKeyHex)) + if err != nil { + t.Fatal(err) + } + entity := kring[0] + + const timeFormat = "2006-01-02" + time1, _ := time.Parse(timeFormat, "2013-07-02") + + // The expiringKeyHex key is structured as: + // + // pub 1024R/5E237D8C created: 2013-07-01 expires: 2013-07-31 usage: SC + // sub 1024R/1ABB25A0 created: 2013-07-01 23:11:07 +0200 CEST expires: 2013-07-08 usage: E + // sub 1024R/96A672F5 created: 2013-07-01 23:11:23 +0200 CEST expires: 2013-07-31 usage: E + // + // So this should select the newest, non-expired encryption key. + key, ok := entity.EncryptionKey(time1, nil) + if !ok { + t.Fatal("No encryption key found") + } + if id, expected := key.PublicKey.KeyIdShortString(), "CD3D39FF"; id != expected { + t.Errorf("Expected key %s at time %s, but got key %s", expected, time1.Format(timeFormat), id) + } + + // Once the first encryption subkey has expired, the second should be + // selected. + time2, _ := time.Parse(timeFormat, "2013-07-09") + key, _ = entity.EncryptionKey(time2, nil) + if id, expected := key.PublicKey.KeyIdShortString(), "CD3D39FF"; id != expected { + t.Errorf("Expected key %s at time %s, but got key %s", expected, time2.Format(timeFormat), id) + } + + // Once all the keys have expired, nothing should be returned. + time3, _ := time.Parse(timeFormat, "2013-08-01") + if key, ok := entity.EncryptionKey(time3, nil); ok { + t.Errorf("Expected no key at time %s, but got key %s", time3.Format(timeFormat), key.PublicKey.KeyIdShortString()) + } +} + +// https://tests.sequoia-pgp.org/#Certificate_expiration +// P _ U f +func TestExpiringPrimaryUIDKey(t *testing.T) { + // P _ U f + kring, err := ReadArmoredKeyRing(bytes.NewBufferString((expiringPrimaryUIDKey))) + if err != nil { + t.Fatal(err) + } + entity := kring[0] + + const timeFormat string = "2006-01-02" + const expectedKeyID string = "015E7330" + + // Before the primary UID has expired, the primary key should be returned. + time1, err := time.Parse(timeFormat, "2022-02-05") + if err != nil { + t.Fatal(err) + } + key, found := entity.SigningKey(time1, nil) + if !found { + t.Errorf("Signing subkey %s not found at time %s", expectedKeyID, time1.Format(timeFormat)) + } else if observedKeyID := key.PublicKey.KeyIdShortString(); observedKeyID != expectedKeyID { + t.Errorf("Expected key %s at time %s, but got key %s", expectedKeyID, time1.Format(timeFormat), observedKeyID) + } + + // After the primary UID has expired, nothing should be returned. + time2, err := time.Parse(timeFormat, "2022-02-06") + if err != nil { + t.Fatal(err) + } + if key, ok := entity.SigningKey(time2, nil); ok { + t.Errorf("Expected no key at time %s, but got key %s", time2.Format(timeFormat), key.PublicKey.KeyIdShortString()) + } +} + +func TestReturnFirstUnexpiredSigningSubkey(t *testing.T) { + // Make a master key. + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + // First signing subkey does not expire. + err = entity.AddSigningSubkey(nil) + if err != nil { + t.Fatal(err) + } + // Get the first signing subkey (added after the default encryption subkey). + subkey1 := entity.Subkeys[1] + + // Second signing subkey expires in a day. + err = entity.AddSigningSubkey(&packet.Config{ + KeyLifetimeSecs: 24 * 60 * 60, + }) + if err != nil { + t.Fatal(err) + } + // Get the second signing subkey. + subkey2 := entity.Subkeys[2] + + // Before second signing subkey has expired, it should be returned. + time1 := time.Now() + expected := subkey2.PublicKey.KeyIdShortString() + subkey, found := entity.SigningKey(time1, nil) + if !found { + t.Errorf("Signing subkey %s not found at time %s", expected, time1.Format(time.UnixDate)) + } + observed := subkey.PublicKey.KeyIdShortString() + if observed != expected { + t.Errorf("Expected key %s at time %s, but got key %s", expected, time1.Format(time.UnixDate), observed) + } + + // After the second signing subkey has expired, the first one should be returned. + time2 := time1.AddDate(0, 0, 2) + expected = subkey1.PublicKey.KeyIdShortString() + subkey, found = entity.SigningKey(time2, nil) + if !found { + t.Errorf("Signing subkey %s not found at time %s", expected, time2.Format(time.UnixDate)) + } + observed = subkey.PublicKey.KeyIdShortString() + if observed != expected { + t.Errorf("Expected key %s at time %s, but got key %s", expected, time2.Format(time.UnixDate), observed) + } +} + +func TestSignatureExpiry(t *testing.T) { + // Make a master key, and attach it to a keyring. + var keyring EntityList + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + keyring = append(keyring, entity) + + // Make a signature that never expires. + var signatureWriter1 bytes.Buffer + const input string = "Hello, world!" + message := strings.NewReader(input) + err = ArmoredDetachSign(&signatureWriter1, []*Entity{entity}, message, nil) + if err != nil { + t.Fatal(err) + } + + // Make a signature that expires in a day. + var signatureWriter2 bytes.Buffer + message = strings.NewReader(input) + err = ArmoredDetachSign(&signatureWriter2, []*Entity{entity}, message, &SignParams{ + Config: &packet.Config{ + SigLifetimeSecs: 24 * 60 * 60, + }, + }) + if err != nil { + t.Fatal(err) + } + + // Make a time that is day after tomorrow. + futureTime := func() time.Time { + return time.Now().AddDate(0, 0, 2) + } + + // Make a signature that was created in the future. + var signatureWriter3 bytes.Buffer + message = strings.NewReader(input) + err = ArmoredDetachSign(&signatureWriter3, []*Entity{entity}, message, &SignParams{ + Config: &packet.Config{ + Time: futureTime, + }, + }) + if err != nil { + t.Fatal(err) + } + + // Check that the first signature has not expired day after tomorrow. + message = strings.NewReader(input) + signatureReader1 := strings.NewReader(signatureWriter1.String()) + _, _, err = VerifyArmoredDetachedSignature(keyring, message, signatureReader1, &packet.Config{ + Time: futureTime, + }) + if err != nil { + t.Fatal(err) + } + + // Check that the second signature has expired day after tomorrow. + message = strings.NewReader(input) + signatureReader2 := strings.NewReader(signatureWriter2.String()) + const expectedErr string = "openpgp: signature expired" + _, _, observedErr := VerifyArmoredDetachedSignature(keyring, message, signatureReader2, &packet.Config{ + Time: futureTime, + }) + if observedErr.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, observedErr) + } + + // Check that the third signature is also considered expired even now. + message = strings.NewReader(input) + signatureReader3 := strings.NewReader(signatureWriter3.String()) + _, _, observedErr = VerifyArmoredDetachedSignature(keyring, message, signatureReader3, nil) + if observedErr.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, observedErr) + } +} + +func TestMissingCrossSignature(t *testing.T) { + // This public key has a signing subkey, but the subkey does not + // contain a cross-signature. + keys, _ := ReadArmoredKeyRing(bytes.NewBufferString(missingCrossSignatureKey)) + var config *packet.Config + _, err := keys[0].Subkeys[0].Verify(config.Now()) + if err == nil { + t.Fatal("Failed to detect error in keyring with missing cross signature") + } + structural, ok := err.(errors.StructuralError) + if !ok { + t.Fatalf("Unexpected class of error: %T. Wanted StructuralError", err) + } + const expectedMsg = "no valid binding signature found for subkey" + if !strings.Contains(string(structural), expectedMsg) { + t.Fatalf("Unexpected error: %q. Expected it to contain %q", err, expectedMsg) + } +} + +func TestInvalidCrossSignature(t *testing.T) { + // This public key has a signing subkey, and the subkey has an + // embedded cross-signature. However, the cross-signature does + // not correctly validate over the primary and subkey. + keys, _ := ReadArmoredKeyRing(bytes.NewBufferString(invalidCrossSignatureKey)) + var config *packet.Config + _, err := keys[0].Subkeys[0].Verify(config.Now()) + if err == nil { + t.Fatal("Failed to detect error in keyring with an invalid cross signature") + } + structural, ok := err.(errors.StructuralError) + if !ok { + t.Fatalf("Unexpected class of error: %T. Wanted StructuralError", err) + } + const expectedMsg = "no valid binding signature found for subkey" + if !strings.Contains(string(structural), expectedMsg) { + t.Fatalf("Unexpected error: %q. Expected it to contain %q", err, expectedMsg) + } +} + +func TestGoodCrossSignature(t *testing.T) { + // This public key has a signing subkey, and the subkey has an + // embedded cross-signature which correctly validates over the + // primary and subkey. + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(goodCrossSignatureKey)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Failed to accept key with good cross signature, %d", len(keys)) + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Failed to accept good subkey, %d", len(keys[0].Subkeys)) + } +} + +func TestRevokedUserID(t *testing.T) { + // This key contains 2 UIDs, one of which is revoked and has no valid self-signature: + // [ultimate] (1) Golang Gopher + // [ revoked] (2) Golang Gopher + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(revokedUserIDKey)) + if err != nil { + t.Fatal(err) + } + + if len(keys) != 1 { + t.Fatal("Failed to read key with a revoked user id") + } + + identities := keys[0].Identities + + if numIdentities, numExpected := len(identities), 2; numIdentities != numExpected { + t.Errorf("obtained %d identities, expected %d", numIdentities, numExpected) + } + + firstIdentity, found := identities["Golang Gopher "] + if !found { + t.Errorf("missing first identity") + } + + secondIdentity, found := identities["Golang Gopher "] + if !found { + t.Errorf("missing second identity") + } + + if firstIdentity.Revoked(nil, time.Now()) { + t.Errorf("expected first identity not to be revoked") + } + + if !secondIdentity.Revoked(nil, time.Now()) { + t.Errorf("expected second identity to be revoked") + } + + const timeFormat = "2006-01-02" + time1, _ := time.Parse(timeFormat, "2020-01-01") + + if _, found := keys[0].SigningKey(time1, nil); !found { + t.Errorf("Expected SigningKey to return a signing key when one User IDs is revoked") + } + + if _, found := keys[0].EncryptionKey(time1, nil); !found { + t.Errorf("Expected EncryptionKey to return an encryption key when one User IDs is revoked") + } +} + +func TestFirstUserIDRevoked(t *testing.T) { + // Same test as above, but with the User IDs reversed: + // [ revoked] (1) Golang Gopher + // [ultimate] (2) Golang Gopher + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithFirstUserIDRevoked)) + if err != nil { + t.Fatal(err) + } + + if len(keys) != 1 { + t.Fatal("Failed to read key with a revoked user id") + } + + identities := keys[0].Identities + + if numIdentities, numExpected := len(identities), 2; numIdentities != numExpected { + t.Errorf("obtained %d identities, expected %d", numIdentities, numExpected) + } + + firstIdentity, found := identities["Golang Gopher "] + if !found { + t.Errorf("missing first identity") + } + + secondIdentity, found := identities["Golang Gopher "] + if !found { + t.Errorf("missing second identity") + } + + if !firstIdentity.Revoked(nil, time.Now()) { + t.Errorf("expected first identity to be revoked") + } + + if secondIdentity.Revoked(nil, time.Now()) { + t.Errorf("expected second identity not to be revoked") + } + + const timeFormat = "2006-01-02" + time1, _ := time.Parse(timeFormat, "2020-01-01") + + if _, found := keys[0].SigningKey(time1, nil); !found { + t.Errorf("Expected SigningKey to return a signing key when first User IDs is revoked") + } + + if _, found := keys[0].EncryptionKey(time1, nil); !found { + t.Errorf("Expected EncryptionKey to return an encryption key when first User IDs is revoked") + } +} + +func TestOnlyUserIDRevoked(t *testing.T) { + // This key contains 1 UID which is revoked (but also has a self-signature) + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithOnlyUserIDRevoked)) + if err != nil { + t.Fatal(err) + } + + if len(keys) != 1 { + t.Fatal("Failed to read key with a revoked user id") + } + + identities := keys[0].Identities + + if numIdentities, numExpected := len(identities), 1; numIdentities != numExpected { + t.Errorf("obtained %d identities, expected %d", numIdentities, numExpected) + } + + identity, found := identities["Revoked Primary User ID "] + if !found { + t.Errorf("missing identity") + } + + if !identity.Revoked(nil, time.Now()) { + t.Errorf("expected identity to be revoked") + } + + if _, found := keys[0].SigningKey(time.Now(), nil); found { + t.Errorf("Expected SigningKey not to return a signing key when the only User IDs is revoked") + } + + if _, found := keys[0].EncryptionKey(time.Now(), nil); found { + t.Errorf("Expected EncryptionKey not to return an encryption key when the only User IDs is revoked") + } +} + +func TestDummyPrivateKey(t *testing.T) { + // This public key has a signing subkey, but has a dummy placeholder + // instead of the real private key. It's used in scenarios where the + // main private key is withheld and only signing is allowed (e.g. build + // servers). + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(onlySubkeyNoPrivateKey)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Failed to accept key with dummy private key, %d", len(keys)) + } + if !keys[0].PrivateKey.Dummy() { + t.Errorf("Primary private key should be marked as a dummy key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Failed to accept good subkey, %d", len(keys[0].Subkeys)) + } + + // Test serialization of stub private key via entity.SerializePrivate(). + var buf bytes.Buffer + w, err := armor.EncodeWithChecksumOption(&buf, PrivateKeyType, nil, false) + if err != nil { + t.Errorf("Failed top initialise armored key writer") + } + err = keys[0].SerializePrivateWithoutSigning(w, nil) + if err != nil { + t.Errorf("Failed to serialize entity") + } + if w.Close() != nil { + t.Errorf("Failed to close writer for armored key") + } + + keys, err = ReadArmoredKeyRing(bytes.NewBufferString(buf.String())) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Failed to accept key with dummy private key, %d", len(keys)) + } + if !keys[0].PrivateKey.Dummy() { + t.Errorf("Primary private key should be marked as a dummy key after serialisation") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Failed to accept good subkey, %d", len(keys[0].Subkeys)) + } +} + +// TestExternallyRevokableKey attempts to load and parse a key with a third party revocation permission. +func TestExternallyRevocableKey(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(subkeyUsageHex)) + if err != nil { + t.Fatal(err) + } + + // The 0xA42704B92866382A key can be revoked by 0xBE3893CB843D0FE70C + // according to this signature that appears within the key: + // :signature packet: algo 1, keyid A42704B92866382A + // version 4, created 1396409682, md5len 0, sigclass 0x1f + // digest algo 2, begin of digest a9 84 + // hashed subpkt 2 len 4 (sig created 2014-04-02) + // hashed subpkt 12 len 22 (revocation key: c=80 a=1 f=CE094AA433F7040BB2DDF0BE3893CB843D0FE70C) + // hashed subpkt 7 len 1 (not revocable) + // subpkt 16 len 8 (issuer key ID A42704B92866382A) + // data: [1024 bits] + + id := uint64(0xA42704B92866382A) + keys := kring.KeysById(id) + if len(keys) != 1 { + t.Errorf("Expected to find key id %X, but got %d matches", id, len(keys)) + } +} + +func TestKeyRevocation(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(revokedKeyHex)) + if err != nil { + t.Fatal(err) + } + + if len(kring) != 1 { + t.Fatal("Failed to read key with a sub key") + } + + // revokedKeyHex contains these keys: + // pub 1024R/9A34F7C0 2014-03-25 [revoked: 2014-03-25] + // sub 1024R/1BA3CD60 2014-03-25 [revoked: 2014-03-25] + ids := []uint64{0xA401D9F09A34F7C0, 0x5CD3BE0A1BA3CD60} + + for _, id := range ids { + keys := kring.KeysById(id) + if len(keys) != 1 { + t.Errorf("Expected KeysById to find revoked key %X, but got %d matches", id, len(keys)) + } + } + + signingkey, found := kring[0].SigningKey(time.Now(), nil) + if found { + t.Errorf("Expected SigningKey not to return a signing key for a revoked key, got %X", signingkey.PublicKey.KeyId) + } + + encryptionkey, found := kring[0].EncryptionKey(time.Now(), nil) + if found { + t.Errorf("Expected EncryptionKey not to return an encryption key for a revoked key, got %X", encryptionkey.PublicKey.KeyId) + } +} + +func TestKeyWithRevokedSubKey(t *testing.T) { + // This key contains a revoked sub key: + // pub rsa1024/0x4CBD826C39074E38 2018-06-14 [SC] + // Key fingerprint = 3F95 169F 3FFA 7D3F 2B47 6F0C 4CBD 826C 3907 4E38 + // uid Golang Gopher + // sub rsa1024/0x945DB1AF61D85727 2018-06-14 [S] [revoked: 2018-06-14] + + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithSubKey)) + if err != nil { + t.Fatal(err) + } + + if len(keys) != 1 { + t.Fatal("Failed to read key with a sub key") + } + + identity := keys[0].Identities["Golang Gopher "] + // Test for an issue where Subkey Binding Signatures (RFC 4880 5.2.1) were added to the identity + // preceding the Subkey Packet if the Subkey Packet was followed by more than one signature. + // For example, the current key has the following layout: + // PUBKEY UID SELFSIG SUBKEY REV SELFSIG + // The last SELFSIG would be added to the UID's signatures. This is wrong. + if numSigs, numExpected := len(identity.SelfCertifications), 1; numSigs != numExpected { + t.Fatalf("got %d signatures, expected %d", numSigs, numExpected) + } + + if numSubKeys, numExpected := len(keys[0].Subkeys), 1; numSubKeys != numExpected { + t.Fatalf("got %d subkeys, expected %d", numSubKeys, numExpected) + } + + subKey := keys[0].Subkeys[0] + if len(subKey.Bindings) == 0 { + t.Fatalf("no binding subkey signature") + } + +} + +func TestSubkeyRevocation(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(revokedSubkeyHex)) + if err != nil { + t.Fatal(err) + } + + if len(kring) != 1 { + t.Fatal("Failed to read key with a sub key") + } + + // revokedSubkeyHex contains these keys: + // pub 1024R/4EF7E4BECCDE97F0 2014-03-25 + // sub 1024R/D63636E2B96AE423 2014-03-25 + // sub 1024D/DBCE4EE19529437F 2014-03-25 + // sub 1024R/677815E371C2FD23 2014-03-25 [revoked: 2014-03-25] + validKeys := []uint64{0x4EF7E4BECCDE97F0, 0xD63636E2B96AE423, 0xDBCE4EE19529437F} + encryptionKey := uint64(0xD63636E2B96AE423) + revokedKey := uint64(0x677815E371C2FD23) + + for _, id := range validKeys { + keys := kring.KeysById(id) + if len(keys) != 1 { + t.Errorf("Expected KeysById to find key %X, but got %d matches", id, len(keys)) + } + if id == encryptionKey { + key, found := kring[0].EncryptionKey(time.Now(), &allowAllAlgorithmsConfig) + if !found || key.PublicKey.KeyId != id { + t.Errorf("Expected EncryptionKey to find key %X", id) + } + } else { + _, found := kring[0].SigningKeyById(time.Now(), id, &allowAllAlgorithmsConfig) + if !found { + t.Errorf("Expected SigningKeyById to find key %X", id) + } + } + } + + keys := kring.KeysById(revokedKey) + if len(keys) != 1 { + t.Errorf("Expected KeysById to find key %X, but got %d matches", revokedKey, len(keys)) + } + + signingkey, found := kring[0].SigningKeyById(time.Now(), revokedKey, nil) + if found { + t.Errorf("Expected SigningKeyById not to return an encryption key for a revoked key, got %X", signingkey.PublicKey.KeyId) + } +} + +func TestKeyWithSubKeyAndBadSelfSigOrder(t *testing.T) { + // This key was altered so that the self signatures following the + // subkey are in a sub-optimal order. + // + // Note: Should someone have to create a similar key again, look into + // gpgsplit, gpg --dearmor, and gpg --enarmor. + // + // The packet ordering is the following: + // PUBKEY UID UIDSELFSIG SUBKEY SELFSIG1 SELFSIG2 + // + // Where: + // SELFSIG1 expires on 2018-06-14 and was created first + // SELFSIG2 does not expire and was created after SELFSIG1 + // + // Test for RFC 4880 5.2.3.3: + // > An implementation that encounters multiple self-signatures on the + // > same object may resolve the ambiguity in any way it sees fit, but it + // > is RECOMMENDED that priority be given to the most recent self- + // > signature. + // + // This means that we should keep SELFSIG2. + + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithSubKeyAndBadSelfSigOrder)) + if err != nil { + t.Fatal(err) + } + + if len(keys) != 1 { + t.Fatal("Failed to read key with a sub key and a bad selfsig packet order") + } + + key := keys[0] + + if numKeys, expected := len(key.Subkeys), 1; numKeys != expected { + t.Fatalf("Read %d subkeys, expected %d", numKeys, expected) + } + + subKey := key.Subkeys[0] + var zeroTime time.Time + selfSig, err := subKey.LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + if lifetime := selfSig.KeyLifetimeSecs; lifetime != nil { + t.Errorf("The signature has a key lifetime (%d), but it should be nil", *lifetime) + } + +} + +func TestIdVerification(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + if err != nil { + t.Fatal(err) + } + if err := kring[1].PrivateKey.Decrypt([]byte("passphrase")); err != nil { + t.Fatal(err) + } + + const signedIdentity = "Test Key 1 (RSA)" + const signerIdentity = "Test Key 2 (RSA, encrypted private key)" + config := allowAllAlgorithmsConfig + config.SigLifetimeSecs = 128 + config.SigningIdentity = signerIdentity + if err := kring[0].SignIdentity(signedIdentity, kring[1], &config); err != nil { + t.Fatal(err) + } + + ident, ok := kring[0].Identities[signedIdentity] + if !ok { + t.Fatal("signed identity missing from key after signing") + } + + checked := false + for _, sig := range ident.OtherCertifications { + if sig.Packet.IssuerKeyId == nil || *sig.Packet.IssuerKeyId != kring[1].PrimaryKey.KeyId { + continue + } + + if err := kring[1].PrimaryKey.VerifyUserIdSignature(signedIdentity, kring[0].PrimaryKey, sig.Packet); err != nil { + t.Fatalf("error verifying new identity signature: %s", err) + } + + if sig.Packet.SignerUserId == nil || *sig.Packet.SignerUserId != signerIdentity { + t.Fatalf("wrong or nil signer identity") + } + + if sig.Packet.SigExpired(time.Now()) { + t.Fatalf("signature is expired") + } + + if !sig.Packet.SigExpired(time.Now().Add(129 * time.Second)) { + t.Fatalf("signature has invalid expiration") + } + + checked = true + break + } + + if !checked { + t.Fatal("didn't find identity signature in Entity") + } +} + +func TestNewEntityWithDefaultHash(t *testing.T) { + for _, hash := range hashes { + c := &packet.Config{ + DefaultHash: hash, + Algorithm: packet.PubKeyAlgoEdDSA, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if hash == crypto.SHA1 { + if err == nil { + t.Fatal("should fail on SHA1 key creation") + } + continue + } + + if err != nil { + t.Fatal(err) + } + + for _, identity := range entity.Identities { + var zeroTime time.Time + selfSig, err := identity.LatestValidSelfCertification(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found ") + } + prefs := selfSig.PreferredHash + if len(prefs) == 0 { + t.Fatal("didn't find a preferred hash list in self signature") + } + ph := hashToHashId(c.DefaultHash) + if c.DefaultHash != crypto.SHA224 && prefs[0] != ph { + t.Fatalf("Expected preferred hash to be %d, got %d", ph, prefs[0]) + } + } + } +} + +func TestNewEntityNilConfigPreferredHash(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + for _, identity := range entity.Identities { + var zeroTime time.Time + selfSig, err := identity.LatestValidSelfCertification(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found ") + } + prefs := selfSig.PreferredHash + if len(prefs) != 1 { + t.Fatal("expected preferred hashes list to be [SHA256]") + } + } +} + +func TestNewEntityCorrectName(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + if len(entity.Identities) != 1 { + t.Fatalf("len(entity.Identities) = %d, want 1", len(entity.Identities)) + } + var got string + for _, i := range entity.Identities { + got = i.Name + } + want := "Golang Gopher (Test Key) " + if got != want { + t.Fatalf("Identity.Name = %q, want %q", got, want) + } +} + +func TestNewEntityWithDefaultCipher(t *testing.T) { + for _, cipher := range ciphers { + c := &packet.Config{ + DefaultCipher: cipher, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if err != nil { + t.Fatal(err) + } + + for _, identity := range entity.Identities { + var zeroTime time.Time + selfSig, err := identity.LatestValidSelfCertification(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found ") + } + prefs := selfSig.PreferredSymmetric + if len(prefs) == 0 { + t.Fatal("didn't find a preferred cipher list") + } + if prefs[0] != uint8(c.DefaultCipher) { + t.Fatalf("Expected preferred cipher to be %d, got %d", uint8(c.DefaultCipher), prefs[0]) + } + } + } +} + +func TestNewEntityNilConfigPreferredSymmetric(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + for _, identity := range entity.Identities { + var zeroTime time.Time + selfSig, err := identity.LatestValidSelfCertification(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found ") + } + prefs := selfSig.PreferredSymmetric + if len(prefs) != 1 || prefs[0] != algorithm.AES128.Id() { + t.Fatal("expected preferred ciphers list to be [AES128]") + } + } +} + +func TestNewEntityWithDefaultAead(t *testing.T) { + for _, aeadMode := range aeadModes { + cfg := &packet.Config{ + AEADConfig: &packet.AEADConfig{ + DefaultMode: aeadMode, + }, + } + entity, err := NewEntity("Botvinnik", "1.e4", "tal@chess.com", cfg) + if err != nil { + t.Fatal(err) + } + + for _, identity := range entity.Identities { + var zeroTime time.Time + selfSig, err := identity.LatestValidSelfCertification(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found ") + } + if len(selfSig.PreferredCipherSuites) == 0 { + t.Fatal("didn't find a preferred mode in self signature") + } + cipher := selfSig.PreferredCipherSuites[0][0] + if cipher != uint8(cfg.Cipher()) { + t.Fatalf("Expected preferred cipher to be %d, got %d", + uint8(cfg.Cipher()), + selfSig.PreferredCipherSuites[0][0]) + } + mode := selfSig.PreferredCipherSuites[0][1] + if mode != uint8(cfg.AEAD().DefaultMode) { + t.Fatalf("Expected preferred mode to be %d, got %d", + uint8(cfg.AEAD().DefaultMode), + selfSig.PreferredCipherSuites[0][1]) + } + } + } +} + +func TestNewEntityPublicSerialization(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + serializedEntity := bytes.NewBuffer(nil) + err = entity.Serialize(serializedEntity) + if err != nil { + t.Fatal(err) + } + + _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } +} + +func TestNewEntityPrivateSerialization(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + + _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } +} + +func TestNotationPacket(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithNotation)) + if err != nil { + t.Fatal(err) + } + + assertNotationPackets(t, keys) + + serializedEntity := bytes.NewBuffer(nil) + err = keys[0].SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + + keys, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + + assertNotationPackets(t, keys) +} + +func assertNotationPackets(t *testing.T, keys EntityList) { + if len(keys) != 1 { + t.Errorf("Failed to accept key, %d", len(keys)) + } + + identity := keys[0].Identities["Test "] + + if numSigs, numExpected := len(identity.SelfCertifications), 1; numSigs != numExpected { + t.Fatalf("got %d signatures, expected %d", numSigs, numExpected) + } + + notations := identity.SelfCertifications[0].Packet.Notations + if numNotations, numExpected := len(notations), 2; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + + if notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + + if notations[0].Name != "text@example.com" { + t.Fatalf("got %s, expected text@example.com", notations[0].Name) + } + + if string(notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(notations[0].Value)) + } + + if notations[1].IsHumanReadable != false { + t.Fatalf("got true, expected false") + } + + if notations[1].Name != "binary@example.com" { + t.Fatalf("got %s, expected binary@example.com", notations[1].Name) + } + + if !bytes.Equal(notations[1].Value, []byte{0, 1, 2, 3}) { + t.Fatalf("got %s, expected {0, 1, 2, 3}", string(notations[1].Value)) + } +} + +func TestEntityPrivateSerialization(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKeyBlock)) + if err != nil { + t.Fatal(err) + } + + for _, entity := range keys { + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + + _, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + } +} + +func TestAddUserId(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddUserId("Golang Gopher", "Test Key", "add1---@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddUserId("Golang Gopher", "Test Key", "add2---@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + ignore_err := entity.AddUserId("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if ignore_err == nil { + t.Fatal(err) + } + + if len(entity.Identities) != 3 { + t.Fatalf("Expected 3 id, got %d", len(entity.Identities)) + } + + for _, sk := range entity.Identities { + var zeroTime time.Time + selfSig, err := sk.LatestValidSelfCertification(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + err = entity.PrimaryKey.VerifyUserIdSignature(sk.UserId.Id, entity.PrimaryKey, selfSig) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } + } + + serializedEntity := bytes.NewBuffer(nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } + + _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } +} +func TestAddSubkey(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(nil) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 3 { + t.Fatalf("Expected 3 subkeys, got %d", len(entity.Subkeys)) + } + + for _, sk := range entity.Subkeys { + var zeroTime time.Time + selfSig, err := sk.LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + err = entity.PrimaryKey.VerifyKeySignature(sk.PublicKey, selfSig) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } + } + + serializedEntity := bytes.NewBuffer(nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } + + _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } +} + +func TestAddSubkeySerialized(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(nil) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + if err := entity.SerializePrivateWithoutSigning(serializedEntity, nil); err != nil { + t.Fatal(err) + } + + entity, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 3 { + t.Fatalf("Expected 3 subkeys, got %d", len(entity.Subkeys)) + } + + for _, sk := range entity.Subkeys { + var zeroTime time.Time + selfSig, err := sk.LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + err = entity.PrimaryKey.VerifyKeySignature(sk.PublicKey, selfSig) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } + } +} + +func TestAddSubkeyWithConfig(t *testing.T) { + c := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEdDSA, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 3 { + t.Fatalf("Expected 3 subkeys, got %d", len(entity.Subkeys)) + } + + if entity.Subkeys[1].PublicKey.PubKeyAlgo != packet.PubKeyAlgoEdDSA { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[1].PublicKey.PubKeyAlgo) + } + + if entity.Subkeys[2].PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoECDH, + entity.Subkeys[2].PublicKey.PubKeyAlgo) + } + + var zeroTime time.Time + selfSig1, err := entity.Subkeys[1].LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + if selfSig1.Hash != c.DefaultHash { + t.Fatalf("Expected subkey hash method: %v, got: %v", c.DefaultHash, + selfSig1.Hash) + } + if selfSig1.EmbeddedSignature.Hash != c.DefaultHash { + t.Fatalf("Expected subkey hash method: %v, got: %v", c.DefaultHash, + selfSig1.EmbeddedSignature.Hash) + } + err = entity.PrimaryKey.VerifyKeySignature(entity.Subkeys[1].PublicKey, selfSig1) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } + + selfSig2, err := entity.Subkeys[2].LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + if selfSig2.Hash != c.DefaultHash { + t.Fatalf("Expected subkey hash method: %v, got: %v", c.DefaultHash, + selfSig2.Hash) + } + err = entity.PrimaryKey.VerifyKeySignature(entity.Subkeys[2].PublicKey, selfSig2) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } + + serializedEntity := bytes.NewBuffer(nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } + + _, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } +} + +func TestAddSubkeyWithConfigSerialized(t *testing.T) { + c := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEdDSA, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + if err := entity.SerializePrivateWithoutSigning(serializedEntity, nil); err != nil { + t.Fatal(err) + } + + entity, err = ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 3 { + t.Fatalf("Expected 3 subkeys, got %d", len(entity.Subkeys)) + } + + var zeroTime time.Time + selfSig1, err := entity.Subkeys[1].LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + if entity.Subkeys[1].PublicKey.PubKeyAlgo != packet.PubKeyAlgoEdDSA { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[1].PublicKey.PubKeyAlgo) + } + + if entity.Subkeys[2].PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoECDH, + entity.Subkeys[2].PublicKey.PubKeyAlgo) + } + + if selfSig1.Hash != c.DefaultHash { + t.Fatalf("Expected subkey hash method: %v, got: %v", c.DefaultHash, + selfSig1.Hash) + } + + if selfSig1.EmbeddedSignature.Hash != c.DefaultHash { + t.Fatalf("Expected subkey hash method: %v, got: %v", c.DefaultHash, + selfSig1.EmbeddedSignature.Hash) + } + + selfSig2, err := entity.Subkeys[2].LatestValidBindingSignature(zeroTime) + if err != nil { + t.Fatal("expected a self signature to be found") + } + if selfSig2.Hash != c.DefaultHash { + t.Fatalf("Expected subkey hash method: %v, got: %v", c.DefaultHash, + selfSig2.Hash) + } + err = entity.PrimaryKey.VerifyKeySignature(entity.Subkeys[1].PublicKey, selfSig1) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } + err = entity.PrimaryKey.VerifyKeySignature(entity.Subkeys[2].PublicKey, selfSig2) + if err != nil { + t.Errorf("Invalid subkey signature: %v", err) + } +} + +func TestRevokeKey(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.Revoke(packet.NoReason, "Key revocation", nil) + if err != nil { + t.Fatal(err) + } + + if len(entity.Revocations) == 0 { + t.Fatal("Revocation signature missing from entity") + } + + for _, r := range entity.Revocations { + err = entity.PrimaryKey.VerifyRevocationSignature(r.Packet) + if err != nil { + t.Errorf("Invalid revocation: %v", err) + } + } +} + +func TestRevokeKeyWithConfig(t *testing.T) { + c := &packet.Config{ + DefaultHash: crypto.SHA512, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{ + Algorithm: packet.PubKeyAlgoEdDSA, + }) + if err != nil { + t.Fatal(err) + } + + err = entity.Revoke(packet.NoReason, "Key revocation", c) + if err != nil { + t.Fatal(err) + } + + if len(entity.Revocations) == 0 { + t.Fatal("Revocation signature missing from entity") + } + + if entity.Revocations[0].Packet.Hash != c.DefaultHash { + t.Fatalf("Expected signature hash method: %v, got: %v", c.DefaultHash, + entity.Revocations[0].Packet.Hash) + } + + for _, r := range entity.Revocations { + err = entity.PrimaryKey.VerifyRevocationSignature(r.Packet) + if err != nil { + t.Errorf("Invalid revocation: %v", err) + } + } +} + +func TestRevokeSubkey(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.Subkeys[0].Revoke(packet.NoReason, "Key revocation", nil) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys[0].Revocations) != 1 { + t.Fatalf("Expected 1 subkey revocation signature, got %v", len(entity.Subkeys[0].Revocations)) + } + + revSig := entity.Subkeys[0].Revocations[0] + + err = entity.PrimaryKey.VerifySubkeyRevocationSignature(revSig.Packet, entity.Subkeys[0].PublicKey) + if err != nil { + t.Fatal(err) + } + + if revSig.Packet.RevocationReason == nil { + t.Fatal("Revocation reason was not set") + } + if revSig.Packet.RevocationReasonText == "" { + t.Fatal("Revocation reason text was not set") + } + + serializedEntity := bytes.NewBuffer(nil) + if err := entity.SerializePrivate(serializedEntity, nil); err != nil { + t.Fatal(err) + } + + // Make sure revocation reason subpackets are not lost during serialization. + newEntity, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if newEntity.Subkeys[0].Revocations[0].Packet.RevocationReason == nil { + t.Fatal("Revocation reason lost after serialization of entity") + } + if newEntity.Subkeys[0].Revocations[0].Packet.RevocationReasonText == "" { + t.Fatal("Revocation reason text lost after serialization of entity") + } +} + +func TestRevokeSubkeyWithInvalidSignature(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + sk := entity.Subkeys[0] + sk.Bindings[0].Packet = &packet.Signature{Version: 4} + + err = sk.Revoke(packet.NoReason, "Key revocation", nil) + if err == nil { + t.Fatal("Entity was able to revoke a subkey with invalid signature") + } +} + +func TestRevokeSubkeyWithConfig(t *testing.T) { + c := &packet.Config{ + DefaultHash: crypto.SHA512, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + sk := entity.Subkeys[0] + err = sk.Revoke(packet.NoReason, "Key revocation", c) + if err != nil { + t.Fatal(err) + } + + if len(sk.Revocations) != 1 { + t.Fatalf("Expected 1 subkey revocation signature, got %v", len(sk.Revocations)) + } + + revSig := sk.Revocations[0].Packet + + if revSig.Hash != c.DefaultHash { + t.Fatalf("Expected signature hash method: %v, got: %v", c.DefaultHash, revSig.Hash) + } + + err = entity.PrimaryKey.VerifySubkeyRevocationSignature(revSig, sk.PublicKey) + if err != nil { + t.Fatal(err) + } +} + +func TestEncryptAndDecryptPrivateKeys(t *testing.T) { + s2kModesToTest := []s2k.Mode{s2k.IteratedSaltedS2K, s2k.Argon2S2K} + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(nil) + if err != nil { + t.Fatal(err) + } + for _, mode := range s2kModesToTest { + t.Run(fmt.Sprintf("S2KMode %d", mode), func(t *testing.T) { + passphrase := []byte("password") + config := &packet.Config{ + S2KConfig: &s2k.Config{ + S2KMode: mode, + }, + } + err = entity.EncryptPrivateKeys(passphrase, config) + if err != nil { + t.Fatal(err) + } + + if !entity.PrivateKey.Encrypted { + t.Fatal("Expected encrypted private key") + } + for _, subkey := range entity.Subkeys { + if !subkey.PrivateKey.Encrypted { + t.Fatal("Expected encrypted private key") + } + } + + err = entity.DecryptPrivateKeys(passphrase) + if err != nil { + t.Fatal(err) + } + + if entity.PrivateKey.Encrypted { + t.Fatal("Expected plaintext private key") + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey.Encrypted { + t.Fatal("Expected plaintext private key") + } + } + }) + } +} + +func TestKeyValidateOnDecrypt(t *testing.T) { + randomPassword := make([]byte, 128) + _, err := rand.Read(randomPassword) + if err != nil { + t.Fatal(err) + } + + t.Run("RSA", func(t *testing.T) { + t.Run("Hardcoded:2048 bits", func(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(rsa2048PrivateKey)) + if err != nil { + t.Fatal("Unable to parse hardcoded key: ", err) + } + + if err := keys[0].PrivateKey.Decrypt([]byte("password")); err != nil { + t.Fatal("Unable to decrypt hardcoded key: ", err) + } + + testKeyValidateRsaOnDecrypt(t, keys[0], randomPassword) + }) + + for _, bits := range []int{2048, 3072, 4096} { + t.Run("Generated:"+strconv.Itoa(bits)+" bits", func(t *testing.T) { + key := testGenerateRSA(t, bits) + testKeyValidateRsaOnDecrypt(t, key, randomPassword) + }) + } + }) + + t.Run("ECDSA", func(t *testing.T) { + t.Run("Hardcoded:NIST P-256", func(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(ecdsaPrivateKey)) + if err != nil { + t.Fatal("Unable to parse hardcoded key: ", err) + } + + if err := keys[0].PrivateKey.Decrypt([]byte("password")); err != nil { + t.Fatal("Unable to decrypt hardcoded key: ", err) + } + + if err := keys[0].Subkeys[0].PrivateKey.Decrypt([]byte("password")); err != nil { + t.Fatal("Unable to decrypt hardcoded subkey: ", err) + } + + testKeyValidateEcdsaOnDecrypt(t, keys[0], randomPassword) + }) + + ecdsaCurves := map[string]packet.Curve{ + "NIST P-256": packet.CurveNistP256, + "NIST P-384": packet.CurveNistP384, + "NIST P-521": packet.CurveNistP521, + "Brainpool P-256": packet.CurveBrainpoolP256, + "Brainpool P-384": packet.CurveBrainpoolP384, + "Brainpool P-512": packet.CurveBrainpoolP512, + "SecP256k1": packet.CurveSecP256k1, + } + + for name, curveType := range ecdsaCurves { + t.Run("Generated:"+name, func(t *testing.T) { + key := testGenerateEC(t, packet.PubKeyAlgoECDSA, curveType) + testKeyValidateEcdsaOnDecrypt(t, key, randomPassword) + }) + } + }) + + t.Run("EdDSA", func(t *testing.T) { + eddsaHardcoded := map[string]string{ + "Curve25519": curve25519PrivateKey, + "Curve448": curve448PrivateKey, + } + + for name, skData := range eddsaHardcoded { + t.Run("Hardcoded:"+name, func(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(skData)) + if err != nil { + t.Fatal("Unable to parse hardcoded key: ", err) + } + + testKeyValidateEddsaOnDecrypt(t, keys[0], randomPassword) + }) + } + + eddsaCurves := map[string]packet.Curve{ + "Curve25519": packet.Curve25519, + "Curve448": packet.Curve448, + } + + for name, curveType := range eddsaCurves { + t.Run("Generated:"+name, func(t *testing.T) { + key := testGenerateEC(t, packet.PubKeyAlgoEdDSA, curveType) + testKeyValidateEddsaOnDecrypt(t, key, randomPassword) + }) + } + }) + + t.Run("DSA With El Gamal Subkey", func(t *testing.T) { + testKeyValidateDsaElGamalOnDecrypt(t, randomPassword) + }) +} + +func testGenerateRSA(t *testing.T, bits int) *Entity { + config := &packet.Config{Algorithm: packet.PubKeyAlgoRSA, RSABits: bits} + rsaEntity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + t.Fatal(err) + } + + return rsaEntity +} + +func testKeyValidateRsaOnDecrypt(t *testing.T, rsaEntity *Entity, password []byte) { + var err error + rsaPrimaryKey := rsaEntity.PrivateKey + if err = rsaPrimaryKey.Encrypt(password); err != nil { + t.Fatal(err) + } + if err = rsaPrimaryKey.Decrypt(password); err != nil { + t.Fatal("Valid RSA key was marked as invalid: ", err) + } + + if err = rsaPrimaryKey.Encrypt(password); err != nil { + t.Fatal(err) + } + + // Corrupt public modulo n in primary key + n := rsaPrimaryKey.PublicKey.PublicKey.(*rsa.PublicKey).N + rsaPrimaryKey.PublicKey.PublicKey.(*rsa.PublicKey).N = new(big.Int).Add(n, big.NewInt(2)) + err = rsaPrimaryKey.Decrypt(password) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid RSA key") + } +} + +func testGenerateEC(t *testing.T, algorithm packet.PublicKeyAlgorithm, curve packet.Curve) *Entity { + config := &packet.Config{Algorithm: algorithm, Curve: curve} + rsaEntity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + t.Fatal(err) + } + + return rsaEntity +} + +func testKeyValidateEcdsaOnDecrypt(t *testing.T, ecdsaKey *Entity, password []byte) { + var err error + ecdsaPrimaryKey := ecdsaKey.PrivateKey + + if err = ecdsaPrimaryKey.Encrypt(password); err != nil { + t.Fatal(err) + } + + if err := ecdsaPrimaryKey.Decrypt(password); err != nil { + t.Fatal("Valid ECDSA key was marked as invalid: ", err) + } + + if err = ecdsaPrimaryKey.Encrypt(password); err != nil { + t.Fatal(err) + } + + // Corrupt public X in primary key + X := ecdsaPrimaryKey.PublicKey.PublicKey.(*ecdsa.PublicKey).X + ecdsaPrimaryKey.PublicKey.PublicKey.(*ecdsa.PublicKey).X = new(big.Int).Add(X, big.NewInt(1)) + err = ecdsaPrimaryKey.Decrypt(password) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ECDSA key") + } + + // ECDH + ecdsaSubkey := ecdsaKey.Subkeys[0].PrivateKey + if err = ecdsaSubkey.Encrypt(password); err != nil { + t.Fatal(err) + } + + if err := ecdsaSubkey.Decrypt(password); err != nil { + t.Fatal("Valid ECDH key was marked as invalid: ", err) + } + + if err = ecdsaSubkey.Encrypt(password); err != nil { + t.Fatal(err) + } + + // Corrupt public X in subkey + ecdsaSubkey.PublicKey.PublicKey.(*ecdh.PublicKey).Point[5] ^= 1 + + err = ecdsaSubkey.Decrypt(password) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ECDH key") + } +} + +func testKeyValidateEddsaOnDecrypt(t *testing.T, eddsaEntity *Entity, password []byte) { + var err error + + eddsaPrimaryKey := eddsaEntity.PrivateKey // already encrypted + if err = eddsaPrimaryKey.Encrypt(password); err != nil { + t.Fatal(err) + } + + if err := eddsaPrimaryKey.Decrypt(password); err != nil { + t.Fatal("Valid EdDSA key was marked as invalid: ", err) + } + + if err = eddsaPrimaryKey.Encrypt(password); err != nil { + t.Fatal(err) + } + + pubKey := *eddsaPrimaryKey.PublicKey.PublicKey.(*eddsa.PublicKey) + pubKey.X[10] ^= 1 + err = eddsaPrimaryKey.Decrypt(password) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid EdDSA key") + } + + // ECDH + ecdhSubkey := eddsaEntity.Subkeys[len(eddsaEntity.Subkeys)-1].PrivateKey + if err = ecdhSubkey.Encrypt(password); err != nil { + t.Fatal(err) + } + + if err := ecdhSubkey.Decrypt(password); err != nil { + t.Fatal("Valid ECDH key was marked as invalid: ", err) + } + + if err = ecdhSubkey.Encrypt(password); err != nil { + t.Fatal(err) + } + + // Corrupt public X in subkey + ecdhSubkey.PublicKey.PublicKey.(*ecdh.PublicKey).Point[5] ^= 1 + err = ecdhSubkey.Decrypt(password) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ECDH key") + } +} + +// ...the legacy bits +func testKeyValidateDsaElGamalOnDecrypt(t *testing.T, randomPassword []byte) { + var err error + + dsaKeys, err := ReadArmoredKeyRing(bytes.NewBufferString(dsaPrivateKeyWithElGamalSubkey)) + if err != nil { + t.Fatal(err) + } + dsaPrimaryKey := dsaKeys[0].PrivateKey // already encrypted + if err := dsaPrimaryKey.Decrypt([]byte("password")); err != nil { + t.Fatal("Valid DSA key was marked as invalid: ", err) + } + + if err = dsaPrimaryKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + // corrupt DSA generator + G := dsaPrimaryKey.PublicKey.PublicKey.(*dsa.PublicKey).G + dsaPrimaryKey.PublicKey.PublicKey.(*dsa.PublicKey).G = new(big.Int).Add(G, big.NewInt(1)) + err = dsaPrimaryKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid DSA key") + } + + // ElGamal + elGamalSubkey := dsaKeys[0].Subkeys[0].PrivateKey // already encrypted + if err := elGamalSubkey.Decrypt([]byte("password")); err != nil { + t.Fatal("Valid ElGamal key was marked as invalid: ", err) + } + + if err = elGamalSubkey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + // corrupt ElGamal generator + G = elGamalSubkey.PublicKey.PublicKey.(*elgamal.PublicKey).G + elGamalSubkey.PublicKey.PublicKey.(*elgamal.PublicKey).G = new(big.Int).Add(G, big.NewInt(1)) + err = elGamalSubkey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ElGamal key") + } +} + +var foreignKeysv4 = []string{ + v4Key25519, +} + +func TestReadPrivateForeignV4Key(t *testing.T) { + for _, str := range foreignKeysv4 { + kring, err := ReadArmoredKeyRing(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + checkV4Key(t, kring[0]) + } +} + +func checkV4Key(t *testing.T, ent *Entity) { + key := ent.PrimaryKey + if key.Version != 4 { + t.Errorf("wrong key version %d", key.Version) + } + if len(key.Fingerprint) != 20 { + t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) + } + signatures := ent.Revocations + for _, id := range ent.Identities { + signatures = append(signatures, id.SelfCertifications...) + } + for _, sig := range signatures { + if sig == nil { + continue + } + if sig.Packet.Version != 4 { + t.Errorf("wrong signature version %d", sig.Packet.Version) + } + fgptLen := len(sig.Packet.IssuerFingerprint) + if fgptLen != 20 { + t.Errorf("Wrong fingerprint length in signature: %d", fgptLen) + } + } +} + +// Should not panic (generated with go-fuzz) +func TestCorruptKeys(t *testing.T) { + data := `-----BEGIN PGP PUBLIC KEY BLOCK00000 + +mQ00BF00000BCAD0000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000ABE000G0Dn000000000000000000iQ00BB0BAgAGBCG00000` + ReadArmoredKeyRing(strings.NewReader(data)) +} + +func TestMultiIdentity(t *testing.T) { + data := `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+wsFfBBMBCgCTBYJkmaEQBYkGcC5aBQsJCAcCCRD7/MgqAV5zMEcUAAAAAAAe +ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmd5GIgb297For6bRAvu7GhG +CDiBP/kCEx783kbtyCnqKgYVCgkICwIEFgIDAQIXgAKZAQIbAwIeARYhBNGmbhoj +sYLJmA94jPv8yCoBXnMwAAA/Ywv/ZupLdzk9vNJAQ3ur9ljM/hjxmX3vjeRJOWr0 +zx8y/9niC4lORVPOoCXoj7poEogo7f//mGDwTWMxJ2G4CgbGoDzLAs/vLKSFfspY +RJf/7lUIFqUxjk3cxGA773DUz0mBWJXh4SFQFRxReICpQVgsb/6cNEeTA4HatFus +2O/hRowJBKWkZrKsbQklK2kfGYqO0wMOUTji9cmW+tS4AgMISnTSv5gY7r7QQexG +suBC5DNRXEMWGBQymjVEM4OpsHzY19MQSBgN8GSb920RmKVN8dWYfQceo6qybce+ +lrCimZAqld36Cuzp+vPFXHVJS0Dz64LVbP3Bmoyp6AOmgrexhXgJDblSDvhhOy1j +IhYaox0J8uqxgaWSdqZyJHji5jckL57hdLVagVcG1BBDiD4rkf4PIppGGHZDzPWV +pW6ClLqT3HZsuwGWOMyZqA9wJheRPCe4Ay7LykmKpr559w1ShebUdprxUW1VGCs0 +JIwI70VZAaxnlVmfHRcspF5xLQKUzShTZWNvbmRhcnkgVXNlcklEIDxzZWNvbmRh +cnlAZXhhbXBsZS5vcmc+wsFcBBMBCgCQBYJkmaEQBYkHd9paBQsJCAcCCRD7/Mgq +AV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc1S70i +tVtTrwouzIR95TtBDOFCexf6oTM9W3xCgP1oGwYVCgkICwIEFgIDAQIXgAIbAwIe +ARYhBNGmbhojsYLJmA94jPv8yCoBXnMwAAAergv+NHzowob+0hY7xy+qNgQwfmzJ +iR4uOqAzIzNHQPiIBuUvFFMAo7dnVAb9iBJCtUBZvcdziforWVykukxaXGnDiOib +vBrQhvCKqDN68aQbi/a+QEDCpGQJ0dMtyRTRWZXebHU3M7XiSzouejIUVnqpiLaY +uJMIILx+xK9uc4lKB01ARnkJHthFSihA3vwxYC6IviUomUQxxs7LlwrEL4GKdLy3 +5KBmn24oeG9kHyDXdfHd1urDYzCxSC1RMtUAPs/mtBIqrzSkeW3SrKpDb9X2HRbb +ejFVLvgKCxGmW4bW6pv+WtofCZbdF4PlrbfWitbLTPZDSVLPsrKK7/k+YH3ah/g4 +sjPoMzJQsWTgWISdoeRTjtAmB0WD8XvtQh1CwomcTCwqT1+6CH2hP8Ew033oy5lS +rRKAZTQ6I/zHvLWW1dCSGlBt9gI+TAXOsfzc/b3nbFrqcjJ9oZoDY/7b+1wnjIkA +XVkt4r+7kzpPoFRDdMvvfRx5+xVLGVn80be8NCLZzsDNBF2lnPIBDADWML9cbGMr +p12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvIDEINOQ6A +9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+Uzula/6k +1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AObaifV7wI +hEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT86Rafp1q +KlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh827KVZW4 +lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6vdRBUnkC +aEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76UqVC7KidN +epdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48AEQEAAcLA +9gQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJEPv8yCoB +XnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcSKhIhk/3U +d5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSxcVV2PL9+ +QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14itcv6alKY +8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHVdTrdZ2Cq +nZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+wqMJxfpa1 +lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6VyjP7SXGLw +vfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xjzRTe56iP +eiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PVNEJd3XZR +zaXZE2aAMQ== +=Ty4h +-----END PGP PUBLIC KEY BLOCK-----` + key, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + var config *packet.Config + sig, _ := key[0].PrimaryIdentity(config.Now()) + if err != nil { + t.Fatal(err) + } + if sig.IsPrimaryId == nil || !*sig.IsPrimaryId { + t.Fatal("expected primary identity to be selected") + } +} + +func TestParseKeyWithUnsupportedSubkey(t *testing.T) { + data := `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +ruxTPhR2zcmefHKGU7cCC/uo +=KLLX +-----END PGP PUBLIC KEY BLOCK-----` + _, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } +} + +func TestParseUnsupportedSubkey(t *testing.T) { + data := `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +NciH07RTRuMS/aRhRg4OB8PQROmTnZ+iZS0= +=7DF0 +-----END PGP PUBLIC KEY BLOCK----- +` + _, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } +} diff --git a/openpgp/v2/keys_test_data.go b/openpgp/v2/keys_test_data.go new file mode 100644 index 000000000..4e0fed110 --- /dev/null +++ b/openpgp/v2/keys_test_data.go @@ -0,0 +1,538 @@ +package v2 + +const expiringKeyHex = "c6c04d0451d0c680010800abbb021fd03ffc4e96618901180c3fdcb060ee69eeead97b91256d11420d80b5f1b51930248044130bd300605cf8a05b7a40d3d8cfb0a910be2e3db50dcd50a9c54064c2a5550801daa834ff4480b33d3d3ca495ff8a4e84a886977d17d998f881241a874083d8b995beab555b6d22b8a4817ab17ac3e7304f7d4d2c05c495fb2218348d3bc13651db1d92732e368a9dd7dcefa6eddff30b94706a9aaee47e9d39321460b740c59c6fc3c2fd8ab6c0fb868cb87c0051f0321301fe0f0e1820b15e7fb7063395769b525005c7e30a7ce85984f5cac00504e7b4fdc45d74958de8388436fd5c7ba9ea121f1c851b5911dd1b47a14d81a09e92ef37721e2325b6790011010001cd00c2c07b041001080025050251d0c680050900278d00060b09070803020415080a0203160201021901021b03021e01000a0910e7b484133a890a35ae4b0800a1beb82e7f28eaf5273d6af9d3391314f6280b2b624eaca2851f89a9ebcaf80ac589ebd509f168bc4322106ca2e2ce77a76e071a3c7444787d65216b5f05e82c77928860b92aace3b7d0327db59492f422eb9dfab7249266d37429870b091a98aba8724c2259ebf8f85093f21255eafa75aa841e31d94f2ac891b9755fed455e539044ee69fc47950b80e003fc9f298d695660f28329eaa38037c367efde1727458e514faf990d439a21461b719edaddf9296d3d0647b43ca56cb8dbf63b4fcf8b9968e7928c463470fab3b98e44d0d95645062f94b2d04fe56bd52822b71934db8ce845622c40b92fcbe765a142e7f38b61a6aa9606c8e8858dcd3b6eb1894acec04d0451d1f06b01080088bea67444e1789390e7c0335c86775502d58ec783d99c8ef4e06de235ed3dd4b0467f6f358d818c7d8989d43ec6d69fcbc8c32632d5a1b605e3fa8e41d695fcdcaa535936cd0157f9040dce362519803b908eafe838bb13216c885c6f93e9e8d5745607f0d062322085d6bdc760969149a8ff8dd9f5c18d9bfe2e6f63a06e17694cf1f67587c6fb70e9aebf90ffc528ca3b615ac7c9d4a21ea4f7c06f2e98fbbd90a859b8608bf9ea638e3a54289ce44c283110d0c45fa458de6251cd6e7baf71f80f12c8978340490fd90c92b81736ae902ed958e478dceae2835953d189c45d182aff02ea2be61b81d8e94430f041d638647b43e2fcb45fd512fbf5068b810011010001c2c06504180108000f050251d1f06b050900081095021b0c000a0910e7b484133a890a35e63407fe2ec88d6d1e6c9ce7553ece0cb2524747217bad29f251d33df84599ffcc900141a355abd62126800744068a5e05dc167056aa9205273dc7765a2ed49db15c2a83b8d6e6429c902136f1e12229086c1c10c0053242c2a4ae1930db58163387a48cad64607ff2153c320e42843dec28e3fce90e7399d63ac0affa2fee1f0adc0953c89eb3f46ef1d6c04328ed13b491669d5120a3782e3ffb7c69575fb77eebd108794f4dda9d34be2bae57e8e59ec8ebfda2f6f06104b2321be408ea146e2db482b00c5055c8618de36ac9716f80da2617e225556d0fce61b01c8cea2d1e0ea982c31711060ca370f2739366e1e708f38405d784b49d16a26cf62d152eae734327cec04d0451d1f07b010800d5af91c5e7c2fd8951c8d254eab0c97cdcb66822f868b79b78c366255059a68fd74ebca9adb9b970cd9e586690e6e0756705432306878c897b10a4b4ca0005966f99ac8fa4e6f9caf54bf8e53844544beee9872a7ac64c119cf1393d96e674254b661f61ee975633d0e8a8672531edb6bb8e211204e7754a9efa802342118eee850beea742bac95a3f706cc2024cf6037a308bb68162b2f53b9a6346a96e6d31871a2456186e24a1c7a82b82ac04afdfd57cd7fb9ba77a9c760d40b76a170f7be525e5fb6a9848cc726e806187710d9b190387df28700f321f988a392899f93815cc937f309129eb94d5299c5547cb2c085898e6639496e70d746c9d3fb9881d0011010001c2c06504180108000f050251d1f07b050900266305021b0c000a0910e7b484133a890a35bff207fd10dfe8c4a6ea1dd30568012b6fd6891a763c87ad0f7a1d112aad9e8e3239378a3b85588c235865bac2e614348cb4f216d7217f53b3ef48c192e0a4d31d64d7bfa5faccf21155965fa156e887056db644a05ad08a85cc6152d1377d9e37b46f4ff462bbe68ace2dc586ef90070314576c985d8037c2ba63f0a7dc17a62e15bd77e88bc61d9d00858979709f12304264a4cf4225c5cf86f12c8e19486cb9cdcc69f18f027e5f16f4ca8b50e28b3115eaff3a345acd21f624aef81f6ede515c1b55b26b84c1e32264754eab672d5489b287e7277ea855e0a5ff2aa9e8b8c76d579a964ec225255f4d57bf66639ccb34b64798846943e162a41096a7002ca21c7f56" +const subkeyUsageHex = "988d04533a52bc010400d26af43085558f65b9e7dbc90cb9238015259aed5e954637adcfa2181548b2d0b60c65f1f42ec5081cbf1bc0a8aa4900acfb77070837c58f26012fbce297d70afe96e759ad63531f0037538e70dbf8e384569b9720d99d8eb39d8d0a2947233ed242436cb6ac7dfe74123354b3d0119b5c235d3dd9c9d6c004f8ffaf67ad8583001101000188b7041f010200210502533b8552170c8001ce094aa433f7040bb2ddf0be3893cb843d0fe70c020700000a0910a42704b92866382aa98404009d63d916a27543da4221c60087c33f1c44bec9998c5438018ed370cca4962876c748e94b73eb39c58eb698063f3fd6346d58dd2a11c0247934c4a9d71f24754f7468f96fb24c3e791dd2392b62f626148ad724189498cbf993db2df7c0cdc2d677c35da0f16cb16c9ce7c33b4de65a4a91b1d21a130ae9cc26067718910ef8e2b417556d627261203c756d627261407379642e65642e61753e88b80413010200220502533a52bc021b03060b090807030206150802090a0b0416020301021e01021780000a0910a42704b92866382a47840400c0c2bd04f5fca586de408b395b3c280a278259c93eaaa8b79a53b97003f8ed502a8a00446dd9947fb462677e4fcac0dac2f0701847d15130aadb6cd9e0705ea0cf5f92f129136c7be21a718d46c8e641eb7f044f2adae573e11ae423a0a9ca51324f03a8a2f34b91fa40c3cc764bee4dccadedb54c768ba0469b683ea53f1c29b88d04533a52bc01040099c92a5d6f8b744224da27bc2369127c35269b58bec179de6bbc038f749344222f85a31933224f26b70243c4e4b2d242f0c4777eaef7b5502f9dad6d8bf3aaeb471210674b74de2d7078af497d55f5cdad97c7bedfbc1b41e8065a97c9c3d344b21fc81d27723af8e374bc595da26ea242dccb6ae497be26eea57e563ed517e90011010001889f0418010200090502533a52bc021b0c000a0910a42704b92866382afa1403ff70284c2de8a043ff51d8d29772602fa98009b7861c540535f874f2c230af8caf5638151a636b21f8255003997ccd29747fdd06777bb24f9593bd7d98a3e887689bf902f999915fcc94625ae487e5d13e6616f89090ebc4fdc7eb5cad8943e4056995bb61c6af37f8043016876a958ec7ebf39c43d20d53b7f546cfa83e8d2604b88d04533b8283010400c0b529316dbdf58b4c54461e7e669dc11c09eb7f73819f178ccd4177b9182b91d138605fcf1e463262fabefa73f94a52b5e15d1904635541c7ea540f07050ce0fb51b73e6f88644cec86e91107c957a114f69554548a85295d2b70bd0b203992f76eb5d493d86d9eabcaa7ef3fc7db7e458438db3fcdb0ca1cc97c638439a9170011010001889f0418010200090502533b8283021b0c000a0910a42704b92866382adc6d0400cfff6258485a21675adb7a811c3e19ebca18851533f75a7ba317950b9997fda8d1a4c8c76505c08c04b6c2cc31dc704d33da36a21273f2b388a1a706f7c3378b66d887197a525936ed9a69acb57fe7f718133da85ec742001c5d1864e9c6c8ea1b94f1c3759cebfd93b18606066c063a63be86085b7e37bdbc65f9a915bf084bb901a204533b85cd110400aed3d2c52af2b38b5b67904b0ef73d6dd7aef86adb770e2b153cd22489654dcc91730892087bb9856ae2d9f7ed1eb48f214243fe86bfe87b349ebd7c30e630e49c07b21fdabf78b7a95c8b7f969e97e3d33f2e074c63552ba64a2ded7badc05ce0ea2be6d53485f6900c7860c7aa76560376ce963d7271b9b54638a4028b573f00a0d8854bfcdb04986141568046202192263b9b67350400aaa1049dbc7943141ef590a70dcb028d730371d92ea4863de715f7f0f16d168bd3dc266c2450457d46dcbbf0b071547e5fbee7700a820c3750b236335d8d5848adb3c0da010e998908dfd93d961480084f3aea20b247034f8988eccb5546efaa35a92d0451df3aaf1aee5aa36a4c4d462c760ecd9cebcabfbe1412b1f21450f203fd126687cd486496e971a87fd9e1a8a765fe654baa219a6871ab97768596ab05c26c1aeea8f1a2c72395a58dbc12ef9640d2b95784e974a4d2d5a9b17c25fedacfe551bda52602de8f6d2e48443f5dd1a2a2a8e6a5e70ecdb88cd6e766ad9745c7ee91d78cc55c3d06536b49c3fee6c3d0b6ff0fb2bf13a314f57c953b8f4d93bf88e70418010200090502533b85cd021b0200520910a42704b92866382a47200419110200060502533b85cd000a091042ce2c64bc0ba99214b2009e26b26852c8b13b10c35768e40e78fbbb48bd084100a0c79d9ea0844fa5853dd3c85ff3ecae6f2c9dd6c557aa04008bbbc964cd65b9b8299d4ebf31f41cc7264b8cf33a00e82c5af022331fac79efc9563a822497ba012953cefe2629f1242fcdcb911dbb2315985bab060bfd58261ace3c654bdbbe2e8ed27a46e836490145c86dc7bae15c011f7e1ffc33730109b9338cd9f483e7cef3d2f396aab5bd80efb6646d7e778270ee99d934d187dd98" +const revokedKeyHex = "988d045331ce82010400c4fdf7b40a5477f206e6ee278eaef888ca73bf9128a9eef9f2f1ddb8b7b71a4c07cfa241f028a04edb405e4d916c61d6beabc333813dc7b484d2b3c52ee233c6a79b1eea4e9cc51596ba9cd5ac5aeb9df62d86ea051055b79d03f8a4fa9f38386f5bd17529138f3325d46801514ea9047977e0829ed728e68636802796801be10011010001889f04200102000905025331d0e3021d03000a0910a401d9f09a34f7c042aa040086631196405b7e6af71026b88e98012eab44aa9849f6ef3fa930c7c9f23deaedba9db1538830f8652fb7648ec3fcade8dbcbf9eaf428e83c6cbcc272201bfe2fbb90d41963397a7c0637a1a9d9448ce695d9790db2dc95433ad7be19eb3de72dacf1d6db82c3644c13eae2a3d072b99bb341debba012c5ce4006a7d34a1f4b94b444526567205265766f6b657220283c52656727732022424d204261726973746122204b657920262530305c303e5c29203c72656740626d626172697374612e636f2e61753e88b704130102002205025331ce82021b03060b090807030206150802090a0b0416020301021e01021780000a0910a401d9f09a34f7c0019c03f75edfbeb6a73e7225ad3cc52724e2872e04260d7daf0d693c170d8c4b243b8767bc7785763533febc62ec2600c30603c433c095453ede59ff2fcabeb84ce32e0ed9d5cf15ffcbc816202b64370d4d77c1e9077d74e94a16fb4fa2e5bec23a56d7a73cf275f91691ae1801a976fcde09e981a2f6327ac27ea1fecf3185df0d56889c04100102000605025331cfb5000a0910fe9645554e8266b64b4303fc084075396674fb6f778d302ac07cef6bc0b5d07b66b2004c44aef711cbac79617ef06d836b4957522d8772dd94bf41a2f4ac8b1ee6d70c57503f837445a74765a076d07b829b8111fc2a918423ddb817ead7ca2a613ef0bfb9c6b3562aec6c3cf3c75ef3031d81d95f6563e4cdcc9960bcb386c5d757b104fcca5fe11fc709df884604101102000605025331cfe7000a09107b15a67f0b3ddc0317f6009e360beea58f29c1d963a22b962b80788c3fa6c84e009d148cfde6b351469b8eae91187eff07ad9d08fcaab88d045331ce820104009f25e20a42b904f3fa555530fe5c46737cf7bd076c35a2a0d22b11f7e0b61a69320b768f4a80fe13980ce380d1cfc4a0cd8fbe2d2e2ef85416668b77208baa65bf973fe8e500e78cc310d7c8705cdb34328bf80e24f0385fce5845c33bc7943cf6b11b02348a23da0bf6428e57c05135f2dc6bd7c1ce325d666d5a5fd2fd5e410011010001889f04180102000905025331ce82021b0c000a0910a401d9f09a34f7c0418003fe34feafcbeaef348a800a0d908a7a6809cc7304017d820f70f0474d5e23cb17e38b67dc6dca282c6ca00961f4ec9edf2738d0f087b1d81e4871ef08e1798010863afb4eac4c44a376cb343be929c5be66a78cfd4456ae9ec6a99d97f4e1c3ff3583351db2147a65c0acef5c003fb544ab3a2e2dc4d43646f58b811a6c3a369d1f" +const revokedSubkeyHex = "988d04533121f6010400aefc803a3e4bb1a61c86e8a86d2726c6a43e0079e9f2713f1fa017e9854c83877f4aced8e331d675c67ea83ddab80aacbfa0b9040bb12d96f5a3d6be09455e2a76546cbd21677537db941cab710216b6d24ec277ee0bd65b910f416737ed120f6b93a9d3b306245c8cfd8394606fdb462e5cf43c551438d2864506c63367fc890011010001b41d416c696365203c616c69636540626d626172697374612e636f2e61753e88bb041301020025021b03060b090807030206150802090a0b0416020301021e01021780050253312798021901000a09104ef7e4beccde97f015a803ff5448437780f63263b0df8442a995e7f76c221351a51edd06f2063d8166cf3157aada4923dfc44aa0f2a6a4da5cf83b7fe722ba8ab416c976e77c6b5682e7f1069026673bd0de56ba06fd5d7a9f177607f277d9b55ff940a638c3e68525c67517e2b3d976899b93ca267f705b3e5efad7d61220e96b618a4497eab8d04403d23f8846041011020006050253312910000a09107b15a67f0b3ddc03d96e009f50b6365d86c4be5d5e9d0ea42d5e56f5794c617700a0ab274e19c2827780016d23417ce89e0a2c0d987d889c04100102000605025331cf7a000a0910a401d9f09a34f7c0ee970400aca292f213041c9f3b3fc49148cbda9d84afee6183c8dd6c5ff2600b29482db5fecd4303797be1ee6d544a20a858080fec43412061c9a71fae4039fd58013b4ae341273e6c66ad4c7cdd9e68245bedb260562e7b166f2461a1032f2b38c0e0e5715fb3d1656979e052b55ca827a76f872b78a9fdae64bc298170bfcebedc1271b41a416c696365203c616c696365407379646973702e6f722e61753e88b804130102002205025331278b021b03060b090807030206150802090a0b0416020301021e01021780000a09104ef7e4beccde97f06a7003fa03c3af68d272ebc1fa08aa72a03b02189c26496a2833d90450801c4e42c5b5f51ad96ce2d2c9cef4b7c02a6a2fcf1412d6a2d486098eb762f5010a201819c17fd2888aec8eda20c65a3b75744de7ee5cc8ac7bfc470cbe3cb982720405a27a3c6a8c229cfe36905f881b02ed5680f6a8f05866efb9d6c5844897e631deb949ca8846041011020006050253312910000a09107b15a67f0b3ddc0347bc009f7fa35db59147469eb6f2c5aaf6428accb138b22800a0caa2f5f0874bacc5909c652a57a31beda65eddd5889c04100102000605025331cf7a000a0910a401d9f09a34f7c0316403ff46f2a5c101256627f16384d34a38fb47a6c88ba60506843e532d91614339fccae5f884a5741e7582ffaf292ba38ee10a270a05f139bde3814b6a077e8cd2db0f105ebea2a83af70d385f13b507fac2ad93ff79d84950328bb86f3074745a8b7f9b64990fb142e2a12976e27e8d09a28dc5621f957ac49091116da410ac3cbde1b88d04533121f6010400cbd785b56905e4192e2fb62a720727d43c4fa487821203cf72138b884b78b701093243e1d8c92a0248a6c0203a5a88693da34af357499abacaf4b3309c640797d03093870a323b4b6f37865f6eaa2838148a67df4735d43a90ca87942554cdf1c4a751b1e75f9fd4ce4e97e278d6c1c7ed59d33441df7d084f3f02beb68896c70011010001889f0418010200090502533121f6021b0c000a09104ef7e4beccde97f0b98b03fc0a5ccf6a372995835a2f5da33b282a7d612c0ab2a97f59cf9fff73e9110981aac2858c41399afa29624a7fd8a0add11654e3d882c0fd199e161bdad65e5e2548f7b68a437ea64293db1246e3011cbb94dc1bcdeaf0f2539bd88ff16d95547144d97cead6a8c5927660a91e6db0d16eb36b7b49a3525b54d1644e65599b032b7eb901a204533127a0110400bd3edaa09eff9809c4edc2c2a0ebe52e53c50a19c1e49ab78e6167bf61473bb08f2050d78a5cbbc6ed66aff7b42cd503f16b4a0b99fa1609681fca9b7ce2bbb1a5b3864d6cdda4d7ef7849d156d534dea30fb0efb9e4cf8959a2b2ce623905882d5430b995a15c3b9fe92906086788b891002924f94abe139b42cbbfaaabe42f00a0b65dc1a1ad27d798adbcb5b5ad02d2688c89477b03ff4eebb6f7b15a73b96a96bed201c0e5e4ea27e4c6e2dd1005b94d4b90137a5b1cf5e01c6226c070c4cc999938101578877ee76d296b9aab8246d57049caacf489e80a3f40589cade790a020b1ac146d6f7a6241184b8c7fcde680eae3188f5dcbe846d7f7bdad34f6fcfca08413e19c1d5df83fc7c7c627d493492e009c2f52a80400a2fe82de87136fd2e8845888c4431b032ba29d9a29a804277e31002a8201fb8591a3e55c7a0d0881496caf8b9fb07544a5a4879291d0dc026a0ea9e5bd88eb4aa4947bbd694b25012e208a250d65ddc6f1eea59d3aed3b4ec15fcab85e2afaa23a40ab1ef9ce3e11e1bc1c34a0e758e7aa64deb8739276df0af7d4121f834a9b88e70418010200090502533127a0021b02005209104ef7e4beccde97f047200419110200060502533127a0000a0910dbce4ee19529437fe045009c0b32f5ead48ee8a7e98fac0dea3d3e6c0e2c552500a0ad71fadc5007cfaf842d9b7db3335a8cdad15d3d1a6404009b08e2c68fe8f3b45c1bb72a4b3278cdf3012aa0f229883ad74aa1f6000bb90b18301b2f85372ca5d6b9bf478d235b733b1b197d19ccca48e9daf8e890cb64546b4ce1b178faccfff07003c172a2d4f5ebaba9f57153955f3f61a9b80a4f5cb959908f8b211b03b7026a8a82fc612bfedd3794969bcf458c4ce92be215a1176ab88d045331d144010400a5063000c5aaf34953c1aa3bfc95045b3aab9882b9a8027fecfe2142dc6b47ba8aca667399990244d513dd0504716908c17d92c65e74219e004f7b83fc125e575dd58efec3ab6dd22e3580106998523dea42ec75bf9aa111734c82df54630bebdff20fe981cfc36c76f865eb1c2fb62c9e85bc3a6e5015a361a2eb1c8431578d0011010001889f04280102000905025331d433021d03000a09104ef7e4beccde97f02e5503ff5e0630d1b65291f4882b6d40a29da4616bb5088717d469fbcc3648b8276de04a04988b1f1b9f3e18f52265c1f8b6c85861691c1a6b8a3a25a1809a0b32ad330aec5667cb4262f4450649184e8113849b05e5ad06a316ea80c001e8e71838190339a6e48bbde30647bcf245134b9a97fa875c1d83a9862cae87ffd7e2c4ce3a1b89013d04180102000905025331d144021b0200a809104ef7e4beccde97f09d2004190102000605025331d144000a0910677815e371c2fd23522203fe22ab62b8e7a151383cea3edd3a12995693911426f8ccf125e1f6426388c0010f88d9ca7da2224aee8d1c12135998640c5e1813d55a93df472faae75bef858457248db41b4505827590aeccf6f9eb646da7f980655dd3050c6897feddddaca90676dee856d66db8923477d251712bb9b3186b4d0114daf7d6b59272b53218dd1da94a03ff64006fcbe71211e5daecd9961fba66cdb6de3f914882c58ba5beddeba7dcb950c1156d7fba18c19ea880dccc800eae335deec34e3b84ac75ffa24864f782f87815cda1c0f634b3dd2fa67cea30811d21723d21d9551fa12ccbcfa62b6d3a15d01307b99925707992556d50065505b090aadb8579083a20fe65bd2a270da9b011" + +const missingCrossSignatureKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Charset: UTF-8 + +mQENBFMYynYBCACVOZ3/e8Bm2b9KH9QyIlHGo/i1bnkpqsgXj8tpJ2MIUOnXMMAY +ztW7kKFLCmgVdLIC0vSoLA4yhaLcMojznh/2CcUglZeb6Ao8Gtelr//Rd5DRfPpG +zqcfUo+m+eO1co2Orabw0tZDfGpg5p3AYl0hmxhUyYSc/xUq93xL1UJzBFgYXY54 +QsM8dgeQgFseSk/YvdP5SMx1ev+eraUyiiUtWzWrWC1TdyRa5p4UZg6Rkoppf+WJ +QrW6BWrhAtqATHc8ozV7uJjeONjUEq24roRc/OFZdmQQGK6yrzKnnbA6MdHhqpdo +9kWDcXYb7pSE63Lc+OBa5X2GUVvXJLS/3nrtABEBAAG0F2ludmFsaWQtc2lnbmlu +Zy1zdWJrZXlziQEoBBMBAgASBQJTnKB5AhsBAgsHAhUIAh4BAAoJEO3UDQUIHpI/ +dN4H/idX4FQ1LIZCnpHS/oxoWQWfpRgdKAEM0qCqjMgiipJeEwSQbqjTCynuh5/R +JlODDz85ABR06aoF4l5ebGLQWFCYifPnJZ/Yf5OYcMGtb7dIbqxWVFL9iLMO/oDL +ioI3dotjPui5e+2hI9pVH1UHB/bZ/GvMGo6Zg0XxLPolKQODMVjpjLAQ0YJ3spew +RAmOGre6tIvbDsMBnm8qREt7a07cBJ6XK7xjxYaZHQBiHVxyEWDa6gyANONx8duW +/fhQ/zDTnyVM/ik6VO0Ty9BhPpcEYLFwh5c1ilFari1ta3e6qKo6ZGa9YMk/REhu +yBHd9nTkI+0CiQUmbckUiVjDKKe5AQ0EUxjKdgEIAJcXQeP+NmuciE99YcJoffxv +2gVLU4ZXBNHEaP0mgaJ1+tmMD089vUQAcyGRvw8jfsNsVZQIOAuRxY94aHQhIRHR +bUzBN28ofo/AJJtfx62C15xt6fDKRV6HXYqAiygrHIpEoRLyiN69iScUsjIJeyFL +C8wa72e8pSL6dkHoaV1N9ZH/xmrJ+k0vsgkQaAh9CzYufncDxcwkoP+aOlGtX1gP +WwWoIbz0JwLEMPHBWvDDXQcQPQTYQyj+LGC9U6f9VZHN25E94subM1MjuT9OhN9Y +MLfWaaIc5WyhLFyQKW2Upofn9wSFi8ubyBnv640Dfd0rVmaWv7LNTZpoZ/GbJAMA +EQEAAYkBHwQYAQIACQUCU5ygeQIbAgAKCRDt1A0FCB6SP0zCB/sEzaVR38vpx+OQ +MMynCBJrakiqDmUZv9xtplY7zsHSQjpd6xGflbU2n+iX99Q+nav0ETQZifNUEd4N +1ljDGQejcTyKD6Pkg6wBL3x9/RJye7Zszazm4+toJXZ8xJ3800+BtaPoI39akYJm ++ijzbskvN0v/j5GOFJwQO0pPRAFtdHqRs9Kf4YanxhedB4dIUblzlIJuKsxFit6N +lgGRblagG3Vv2eBszbxzPbJjHCgVLR3RmrVezKOsZjr/2i7X+xLWIR0uD3IN1qOW +CXQxLBizEEmSNVNxsp7KPGTLnqO3bPtqFirxS9PJLIMPTPLNBY7ZYuPNTMqVIUWF +4artDmrG +=7FfJ +-----END PGP PUBLIC KEY BLOCK-----` + +const invalidCrossSignatureKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFMYynYBCACVOZ3/e8Bm2b9KH9QyIlHGo/i1bnkpqsgXj8tpJ2MIUOnXMMAY +ztW7kKFLCmgVdLIC0vSoLA4yhaLcMojznh/2CcUglZeb6Ao8Gtelr//Rd5DRfPpG +zqcfUo+m+eO1co2Orabw0tZDfGpg5p3AYl0hmxhUyYSc/xUq93xL1UJzBFgYXY54 +QsM8dgeQgFseSk/YvdP5SMx1ev+eraUyiiUtWzWrWC1TdyRa5p4UZg6Rkoppf+WJ +QrW6BWrhAtqATHc8ozV7uJjeONjUEq24roRc/OFZdmQQGK6yrzKnnbA6MdHhqpdo +9kWDcXYb7pSE63Lc+OBa5X2GUVvXJLS/3nrtABEBAAG0F2ludmFsaWQtc2lnbmlu +Zy1zdWJrZXlziQEoBBMBAgASBQJTnKB5AhsBAgsHAhUIAh4BAAoJEO3UDQUIHpI/ +dN4H/idX4FQ1LIZCnpHS/oxoWQWfpRgdKAEM0qCqjMgiipJeEwSQbqjTCynuh5/R +JlODDz85ABR06aoF4l5ebGLQWFCYifPnJZ/Yf5OYcMGtb7dIbqxWVFL9iLMO/oDL +ioI3dotjPui5e+2hI9pVH1UHB/bZ/GvMGo6Zg0XxLPolKQODMVjpjLAQ0YJ3spew +RAmOGre6tIvbDsMBnm8qREt7a07cBJ6XK7xjxYaZHQBiHVxyEWDa6gyANONx8duW +/fhQ/zDTnyVM/ik6VO0Ty9BhPpcEYLFwh5c1ilFari1ta3e6qKo6ZGa9YMk/REhu +yBHd9nTkI+0CiQUmbckUiVjDKKe5AQ0EUxjKdgEIAIINDqlj7X6jYKc6DjwrOkjQ +UIRWbQQar0LwmNilehmt70g5DCL1SYm9q4LcgJJ2Nhxj0/5qqsYib50OSWMcKeEe +iRXpXzv1ObpcQtI5ithp0gR53YPXBib80t3bUzomQ5UyZqAAHzMp3BKC54/vUrSK +FeRaxDzNLrCeyI00+LHNUtwghAqHvdNcsIf8VRumK8oTm3RmDh0TyjASWYbrt9c8 +R1Um3zuoACOVy+mEIgIzsfHq0u7dwYwJB5+KeM7ZLx+HGIYdUYzHuUE1sLwVoELh ++SHIGHI1HDicOjzqgajShuIjj5hZTyQySVprrsLKiXS6NEwHAP20+XjayJ/R3tEA +EQEAAYkCPgQYAQIBKAUCU5ygeQIbAsBdIAQZAQIABgUCU5ygeQAKCRCpVlnFZmhO +52RJB/9uD1MSa0wjY6tHOIgquZcP3bHBvHmrHNMw9HR2wRCMO91ZkhrpdS3ZHtgb +u3/55etj0FdvDo1tb8P8FGSVtO5Vcwf5APM8sbbqoi8L951Q3i7qt847lfhu6sMl +w0LWFvPTOLHrliZHItPRjOltS1WAWfr2jUYhsU9ytaDAJmvf9DujxEOsN5G1YJep +54JCKVCkM/y585Zcnn+yxk/XwqoNQ0/iJUT9qRrZWvoeasxhl1PQcwihCwss44A+ +YXaAt3hbk+6LEQuZoYS73yR3WHj+42tfm7YxRGeubXfgCEz/brETEWXMh4pe0vCL +bfWrmfSPq2rDegYcAybxRQz0lF8PAAoJEO3UDQUIHpI/exkH/0vQfdHA8g/N4T6E +i6b1CUVBAkvtdJpCATZjWPhXmShOw62gkDw306vHPilL4SCvEEi4KzG72zkp6VsB +DSRcpxCwT4mHue+duiy53/aRMtSJ+vDfiV1Vhq+3sWAck/yUtfDU9/u4eFaiNok1 +8/Gd7reyuZt5CiJnpdPpjCwelK21l2w7sHAnJF55ITXdOxI8oG3BRKufz0z5lyDY +s2tXYmhhQIggdgelN8LbcMhWs/PBbtUr6uZlNJG2lW1yscD4aI529VjwJlCeo745 +U7pO4eF05VViUJ2mmfoivL3tkhoTUWhx8xs8xCUcCg8DoEoSIhxtOmoTPR22Z9BL +6LCg2mg= +=Dhm4 +-----END PGP PUBLIC KEY BLOCK-----` + +const goodCrossSignatureKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mI0EVUqeVwEEAMufHRrMPWK3gyvi0O0tABCs/oON9zV9KDZlr1a1M91ShCSFwCPo +7r80PxdWVWcj0V5h50/CJYtpN3eE/mUIgW2z1uDYQF1OzrQ8ubrksfsJvpAhENom +lTQEppv9mV8qhcM278teb7TX0pgrUHLYF5CfPdp1L957JLLXoQR/lwLVABEBAAG0 +E2dvb2Qtc2lnbmluZy1zdWJrZXmIuAQTAQIAIgUCVUqeVwIbAwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AACgkQNRjL95IRWP69XQQAlH6+eyXJN4DZTLX78KGjHrsw +6FCvxxClEPtPUjcJy/1KCRQmtLAt9PbbA78dvgzjDeZMZqRAwdjyJhjyg/fkU2OH +7wq4ktjUu+dLcOBb+BFMEY+YjKZhf6EJuVfxoTVr5f82XNPbYHfTho9/OABKH6kv +X70PaKZhbwnwij8Nts65AaIEVUqftREEAJ3WxZfqAX0bTDbQPf2CMT2IVMGDfhK7 +GyubOZgDFFjwUJQvHNvsrbeGLZ0xOBumLINyPO1amIfTgJNm1iiWFWfmnHReGcDl +y5mpYG60Mb79Whdcer7CMm3AqYh/dW4g6IB02NwZMKoUHo3PXmFLxMKXnWyJ0clw +R0LI/Qn509yXAKDh1SO20rqrBM+EAP2c5bfI98kyNwQAi3buu94qo3RR1ZbvfxgW +CKXDVm6N99jdZGNK7FbRifXqzJJDLcXZKLnstnC4Sd3uyfyf1uFhmDLIQRryn5m+ +LBYHfDBPN3kdm7bsZDDq9GbTHiFZUfm/tChVKXWxkhpAmHhU/tH6GGzNSMXuIWSO +aOz3Rqq0ED4NXyNKjdF9MiwD/i83S0ZBc0LmJYt4Z10jtH2B6tYdqnAK29uQaadx +yZCX2scE09UIm32/w7pV77CKr1Cp/4OzAXS1tmFzQ+bX7DR+Gl8t4wxr57VeEMvl +BGw4Vjh3X8//m3xynxycQU18Q1zJ6PkiMyPw2owZ/nss3hpSRKFJsxMLhW3fKmKr +Ey2KiOcEGAECAAkFAlVKn7UCGwIAUgkQNRjL95IRWP5HIAQZEQIABgUCVUqftQAK +CRD98VjDN10SqkWrAKDTpEY8D8HC02E/KVC5YUI01B30wgCgurpILm20kXEDCeHp +C5pygfXw1DJrhAP+NyPJ4um/bU1I+rXaHHJYroYJs8YSweiNcwiHDQn0Engh/mVZ +SqLHvbKh2dL/RXymC3+rjPvQf5cup9bPxNMa6WagdYBNAfzWGtkVISeaQW+cTEp/ +MtgVijRGXR/lGLGETPg2X3Afwn9N9bLMBkBprKgbBqU7lpaoPupxT61bL70= +=vtbN +-----END PGP PUBLIC KEY BLOCK-----` + +const revokedUserIDKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +7qTZOahrETw= +=IKnw +-----END PGP PUBLIC KEY BLOCK-----` + +const keyWithFirstUserIDRevoked = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP.js v4.10.10 +Comment: https://openpgpjs.org + +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 +=+2T8 +-----END PGP PUBLIC KEY BLOCK----- +` + +const keyWithOnlyUserIDRevoked = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYYwB7RYJKwYBBAHaRw8BAQdARimqhPPzyGAXmfQJjcqM1QVPzLtURJSzNVll +JV4tEaW0KVJldm9rZWQgUHJpbWFyeSBVc2VyIElEIDxyZXZva2VkQGtleS5jb20+ +iHgEMBYIACAWIQSpyJZAXYqVEFkjyKutFcS0yeB0LQUCYYwCtgIdAAAKCRCtFcS0 +yeB0LbSsAQD8OYMaaBjrdzzpwIkP1stgmPd4/kzN/ZG28Ywl6a5F5QEA5Xg7aq4e +/t6Fsb4F5iqB956kSPe6YJrikobD/tBbMwSIkAQTFggAOBYhBKnIlkBdipUQWSPI +q60VxLTJ4HQtBQJhjAHtAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEK0V +xLTJ4HQtBaoBAPZL7luTCji+Tqhn7XNfFE/0QIahCt8k9wfO1cGlB3inAQDf8Tzw +ZGR5fNluUcNoVxQT7bUSFStbaGo3k0BaOYPbCLg4BGGMAe0SCisGAQQBl1UBBQEB +B0DLwSpveSrbIO/IVZD13yrs1XuB3FURZUnafGrRq7+jUAMBCAeIeAQYFggAIBYh +BKnIlkBdipUQWSPIq60VxLTJ4HQtBQJhjAHtAhsMAAoJEK0VxLTJ4HQtZ1oA/j9u +8+p3xTNzsmabTL6BkNbMeB/RUKCrlm6woM6AV+vxAQCcXTn3JC2sNoNrLoXuVzaA +mcG3/TwG5GSQUUPkrDsGDA== +=mFWy +-----END PGP PUBLIC KEY BLOCK----- +` + +const keyWithSubKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EWyKwKQEEALwXhKBnyaaNFeK3ljfc/qn9X/QFw+28EUfgZPHjRmHubuXLE2uR +s3ZoSXY2z7Dkv+NyHYMt8p+X8q5fR7JvUjK2XbPyKoiJVnHINll83yl67DaWfKNL +EjNoO0kIfbXfCkZ7EG6DL+iKtuxniGTcnGT47e+HJSqb/STpLMnWwXjBABEBAAG0 +I0dvbGFuZyBHb3BoZXIgPG5vLXJlcGx5QGdvbGFuZy5jb20+iM4EEwEKADgWIQQ/ +lRafP/p9PytHbwxMvYJsOQdOOAUCWyKwKQIbAwULCQgHAwUVCgkICwUWAgMBAAIe +AQIXgAAKCRBMvYJsOQdOOOsFBAC62mXww8XuqvYLcVOvHkWLT6mhxrQOJXnlfpn7 +2uBV9CMhoG/Ycd43NONsJrB95Apr9TDIqWnVszNbqPCuBhZQSGLdbiDKjxnCWBk0 +69qv4RNtkpOhYB7jK4s8F5oQZqId6JasT/PmJTH92mhBYhhTQr0GYFuPX2UJdkw9 +Sn9C67iNBFsisDUBBAC3A+Yo9lgCnxi/pfskyLrweYif6kIXWLAtLTsM6g/6jt7b +wTrknuCPyTv0QKGXsAEe/cK/Xq3HvX9WfXPGIHc/X56ZIsHQ+RLowbZV/Lhok1IW +FAuQm8axr/by80cRwFnzhfPc/ukkAq2Qyj4hLsGblu6mxeAhzcp8aqmWOO2H9QAR +AQABiLYEKAEKACAWIQQ/lRafP/p9PytHbwxMvYJsOQdOOAUCWyK16gIdAAAKCRBM +vYJsOQdOOB1vA/4u4uLONsE+2GVOyBsHyy7uTdkuxaR9b54A/cz6jT/tzUbeIzgx +22neWhgvIEghnUZd0vEyK9k1wy5vbDlEo6nKzHso32N1QExGr5upRERAxweDxGOj +7luDwNypI7QcifE64lS/JmlnunwRCdRWMKc0Fp+7jtRc5mpwyHN/Suf5RokBagQY +AQoAIBYhBD+VFp8/+n0/K0dvDEy9gmw5B044BQJbIrA1AhsCAL8JEEy9gmw5B044 +tCAEGQEKAB0WIQSNdnkaWY6t62iX336UXbGvYdhXJwUCWyKwNQAKCRCUXbGvYdhX +JxJSA/9fCPHP6sUtGF1o3G1a3yvOUDGr1JWcct9U+QpbCt1mZoNopCNDDQAJvDWl +mvDgHfuogmgNJRjOMznvahbF+wpTXmB7LS0SK412gJzl1fFIpK4bgnhu0TwxNsO1 +8UkCZWqxRMgcNUn9z6XWONK8dgt5JNvHSHrwF4CxxwjL23AAtK+FA/UUoi3U4kbC +0XnSr1Sl+mrzQi1+H7xyMe7zjqe+gGANtskqexHzwWPUJCPZ5qpIa2l8ghiUim6b +4ymJ+N8/T8Yva1FaPEqfMzzqJr8McYFm0URioXJPvOAlRxdHPteZ0qUopt/Jawxl +Xt6B9h1YpeLoJwjwsvbi98UTRs0jXwoY +=3fWu +-----END PGP PUBLIC KEY BLOCK-----` + +const keyWithSubKeyAndBadSelfSigOrder = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EWyLLDQEEAOqIOpJ/ha1OYAGduu9tS3rBz5vyjbNgJO4sFveEM0mgsHQ0X9/L +plonW+d0gRoO1dhJ8QICjDAc6+cna1DE3tEb5m6JtQ30teLZuqrR398Cf6w7NNVz +r3lrlmnH9JaKRuXl7tZciwyovneBfZVCdtsRZjaLI1uMQCz/BToiYe3DABEBAAG0 +I0dvbGFuZyBHb3BoZXIgPG5vLXJlcGx5QGdvbGFuZy5jb20+iM4EEwEKADgWIQRZ +sixZOfQcZdW0wUqmgmdsv1O9xgUCWyLLDQIbAwULCQgHAwUVCgkICwUWAgMBAAIe +AQIXgAAKCRCmgmdsv1O9xql2A/4pix98NxjhdsXtazA9agpAKeADf9tG4Za27Gj+ +3DCww/E4iP2X35jZimSm/30QRB6j08uGCqd9vXkkJxtOt63y/IpVOtWX6vMWSTUm +k8xKkaYMP0/IzKNJ1qC/qYEUYpwERBKg9Z+k99E2Ql4kRHdxXUHq6OzY79H18Y+s +GdeM/riNBFsiyxsBBAC54Pxg/8ZWaZX1phGdwfe5mek27SOYpC0AxIDCSOdMeQ6G +HPk38pywl1d+S+KmF/F4Tdi+kWro62O4eG2uc/T8JQuRDUhSjX0Qa51gPzJrUOVT +CFyUkiZ/3ZDhtXkgfuso8ua2ChBgR9Ngr4v43tSqa9y6AK7v0qjxD1x+xMrjXQAR +AQABiQFxBBgBCgAmAhsCFiEEWbIsWTn0HGXVtMFKpoJnbL9TvcYFAlsizTIFCQAN +MRcAv7QgBBkBCgAdFiEEJcoVUVJIk5RWj1c/o62jUpRPICQFAlsiyxsACgkQo62j +UpRPICQq5gQApoWIigZxXFoM0uw4uJBS5JFZtirTANvirZV5RhndwHeMN6JttaBS +YnjyA4+n1D+zB2VqliD2QrsX12KJN6rGOehCtEIClQ1Hodo9nC6kMzzAwW1O8bZs +nRJmXV+bsvD4sidLZLjdwOVa3Cxh6pvq4Uur6a7/UYx121hEY0Qx0s8JEKaCZ2y/ +U73GGi0D/i20VW8AWYAPACm2zMlzExKTOAV01YTQH/3vW0WLrOse53WcIVZga6es +HuO4So0SOEAvxKMe5HpRIu2dJxTvd99Bo9xk9xJU0AoFrO0vNCRnL+5y68xMlODK +lEw5/kl0jeaTBp6xX0HDQOEVOpPGUwWV4Ij2EnvfNDXaE1vK1kffiQFrBBgBCgAg +AhsCFiEEWbIsWTn0HGXVtMFKpoJnbL9TvcYFAlsi0AYAv7QgBBkBCgAdFiEEJcoV +UVJIk5RWj1c/o62jUpRPICQFAlsiyxsACgkQo62jUpRPICQq5gQApoWIigZxXFoM +0uw4uJBS5JFZtirTANvirZV5RhndwHeMN6JttaBSYnjyA4+n1D+zB2VqliD2QrsX +12KJN6rGOehCtEIClQ1Hodo9nC6kMzzAwW1O8bZsnRJmXV+bsvD4sidLZLjdwOVa +3Cxh6pvq4Uur6a7/UYx121hEY0Qx0s8JEKaCZ2y/U73GRl0EAJokkXmy4zKDHWWi +wvK9gi2gQgRkVnu2AiONxJb5vjeLhM/07BRmH6K1o+w3fOeEQp4FjXj1eQ5fPSM6 +Hhwx2CTl9SDnPSBMiKXsEFRkmwQ2AAsQZLmQZvKBkLZYeBiwf+IY621eYDhZfo+G +1dh1WoUCyREZsJQg2YoIpWIcvw+a +=bNRo +-----END PGP PUBLIC KEY BLOCK----- +` + +const onlySubkeyNoPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQCVBFggvocBBAC7vBsHn7MKmS6IiiZNTXdciplVgS9cqVd+RTdIAoyNTcsiV1H0 +GQ3QtodOPeDlQDNoqinqaobd7R9g3m3hS53Nor7yBZkCWQ5x9v9JxRtoAq0sklh1 +I1X2zEqZk2l6YrfBF/64zWrhjnW3j23szkrAIVu0faQXbQ4z56tmZrw11wARAQAB +/gdlAkdOVQG0CUdOVSBEdW1teYi4BBMBAgAiBQJYIL6HAhsDBgsJCAcDAgYVCAIJ +CgsEFgIDAQIeAQIXgAAKCRCd1xxWp1CYAnjGA/9synn6ZXJUKAXQzySgmCZvCIbl +rqBfEpxwLG4Q/lONhm5vthAE0z49I8hj5Gc5e2tLYUtq0o0OCRdCrYHa/efOYWpJ +6RsK99bePOisVzmOABLIgZkcr022kHoMCmkPgv9CUGKP1yqbGl+zzAwQfUjRUmvD +ZIcWLHi2ge4GzPMPi50B2ARYIL6cAQQAxWHnicKejAFcFcF1/3gUSgSH7eiwuBPX +M7vDdgGzlve1o1jbV4tzrjN9jsCl6r0nJPDMfBSzgLr1auNTRG6HpJ4abcOx86ED +Ad+avDcQPZb7z3dPhH/gb2lQejZsHh7bbeOS8WMSzHV3RqCLd8J/xwWPNR5zKn1f +yp4IGfopidMAEQEAAQAD+wQOelnR82+dxyM2IFmZdOB9wSXQeCVOvxSaNMh6Y3lk +UOOkO8Nlic4x0ungQRvjoRs4wBmCuwFK/MII6jKui0B7dn/NDf51i7rGdNGuJXDH +e676By1sEY/NGkc74jr74T+5GWNU64W0vkpfgVmjSAzsUtpmhJMXsc7beBhJdnVl +AgDKCb8hZqj1alcdmLoNvb7ibA3K/V8J462CPD7bMySPBa/uayoFhNxibpoXml2r +oOtHa5izF3b0/9JY97F6rqkdAgD6GdTJ+xmlCoz1Sewoif1I6krq6xoa7gOYpIXo +UL1Afr+LiJeyAnF/M34j/kjIVmPanZJjry0kkjHE5ILjH3uvAf4/6n9np+Th8ujS +YDCIzKwR7639+H+qccOaddCep8Y6KGUMVdD/vTKEx1rMtK+hK/CDkkkxnFslifMJ +kqoqv3WUqCWJAT0EGAECAAkFAlggvpwCGwIAqAkQndccVqdQmAKdIAQZAQIABgUC +WCC+nAAKCRDmGUholQPwvQk+A/9latnSsR5s5/1A9TFki11GzSEnfLbx46FYOdkW +n3YBxZoPQGxNA1vIn8GmouxZInw9CF4jdOJxEdzLlYQJ9YLTLtN5tQEMl/19/bR8 +/qLacAZ9IOezYRWxxZsyn6//jfl7A0Y+FV59d4YajKkEfItcIIlgVBSW6T+TNQT3 +R+EH5HJ/A/4/AN0CmBhhE2vGzTnVU0VPrE4V64pjn1rufFdclgpixNZCuuqpKpoE +VVHn6mnBf4njKjZrAGPs5kfQ+H4NsM7v3Zz4yV6deu9FZc4O6E+V1WJ38rO8eBix +7G2jko106CC6vtxsCPVIzY7aaG3H5pjRtomw+pX7SzrQ7FUg2PGumg== +=F/T0 +-----END PGP PRIVATE KEY BLOCK-----` + +const ecdsaPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xaUEX1KsSRMIKoZIzj0DAQcCAwTpYqJsnJiFhKKh+8TulWD+lVmerBFNS+Ii +B+nlG3T0xQQ4Sy5eIjJ0CExIQQzi3EElF/Z2l4F3WC5taFA11NgA/gkDCHSS +PThf1M2K4LN8F1MRcvR+sb7i0nH55ojkwuVB1DE6jqIT9m9i+mX1tzjSAS+6 +lPQiweCJvG7xTC7Hs3AzRapf/r1At4TB+v+5G2/CKynNFEJpbGwgPGJpbGxA +aG9tZS5jb20+wncEEBMIAB8FAl9SrEkGCwkHCAMCBBUICgIDFgIBAhkBAhsD +Ah4BAAoJEMpwT3+q3+xqw5UBAMebZN9isEZ1ML+R/jWAAWMwa/knMugrEZ1v +Bl9+ZwM0AQCZdf80/wYY4Nve01qSRFv8OmKswLli3TvDv6FKc4cLz8epBF9S +rEkSCCqGSM49AwEHAgMEAjKnT9b5wY2bf9TpAV3d7OUfPOxKj9c4VzeVzSrH +AtQgo/MuI1cdYVURicV4i76DNjFhQHQFTk7BrC+C2u1yqQMBCAf+CQMIHImA +iYfzQtjgQWSFZYUkCFpbbwhNF0ch+3HNaZkaHCnZRIsWsRnc6FCb6lRQyK9+ +Dq59kHlduE5QgY40894jfmP2JdJHU6nBdYrivbEdbMJhBBgTCAAJBQJfUqxJ +AhsMAAoJEMpwT3+q3+xqUI0BAMykhV08kQ4Ip9Qlbss6Jdufv7YrU0Vd5hou +b5TmiPd0APoDBh3qIic+aLLUcAuG3+Gt1P1AbUlmqV61ozn1WfHxfw== +=KLN8 +-----END PGP PRIVATE KEY BLOCK-----` + +const dsaPrivateKeyWithElGamalSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +4g== +=XZm8 +-----END PGP PRIVATE KEY BLOCK-----` + +// https://tests.sequoia-pgp.org/#Certificate_expiration +// P _ U p +const expiringPrimaryUIDKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=AmgT +-----END PGP PUBLIC KEY BLOCK-----` + +const rsa2048PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: gpg (GnuPG) 2.2.27 with libgcrypt 1.9.4 + +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 +=u442 +-----END PGP PRIVATE KEY BLOCK-----` + +const curve25519PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: gpg (GnuPG) 2.2.27 with libgcrypt 1.9.4 + +lFgEYvTtQBYJKwYBBAHaRw8BAQdAxsNXLbrk5xOjpO24VhOMvQ0/F+JcyIkckMDH +X3FIGxcAAQDFOlunZWYuPsCx5JLp78vKqUTfgef9TGG4oD6I/Sa0zBMstCJHb2xh +bmcgR29waGVyIDxnb2xhbmdAZXhhbXBsZS5vcmc+iJAEExYIADgWIQSFQHEOazmo +h1ldII4MvfnLQ4JBNwUCYvTtQAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK +CRAMvfnLQ4JBN5yeAQCKdry8B5ScCPrev2+UByMCss7Sdu5RhomCFsHdNPLcKAEA +8ugei+1owHsV+3cGwWWzKk6sLa8ZN87i3SKuOGp9DQycXQRi9O1AEgorBgEEAZdV +AQUBAQdA5CubPp8l7lrVQ25h7Hx5XN2C8xanRnnpcjzEooCaEA0DAQgHAAD/Rpc+ +sOZUXrFk9HOWB1XU41LoWbDBoG8sP8RWAVYwD5AQRYh4BBgWCAAgFiEEhUBxDms5 +qIdZXSCODL35y0OCQTcFAmL07UACGwwACgkQDL35y0OCQTcvdwEA7lb5g/YisrEf +iq660uwMGoepLUfvtqKzuQ6heYe83y0BAN65Ffg5HYOJzUEi0kZQRf7OhdtuL2kJ +SRXn8DmCTfEB +=cELM +-----END PGP PRIVATE KEY BLOCK-----` + +const curve448PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: C1DB 65D5 80D7 B922 7254 4B1E A699 9895 FABA CE52 + +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 +QgqsfguR1PqPuJxpXV4bSr6CGAAAAA== +=MSvh +-----END PGP PRIVATE KEY BLOCK-----` + +const keyWithNotation = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEY9gIshYJKwYBBAHaRw8BAQdAF25fSM8OpFlXZhop4Qpqo5ywGZ4jgWlR +ppjhIKDthREAAQC+LFpzFcMJYcjxGKzBGHN0Px2jU4d04YSRnFAik+lVVQ6u +zRdUZXN0IDx0ZXN0QGV4YW1wbGUuY29tPsLACgQQFgoAfAUCY9gIsgQLCQcI +CRD/utJOCym8pR0UgAAAAAAQAAR0ZXh0QGV4YW1wbGUuY29tdGVzdB8UAAAA +AAASAARiaW5hcnlAZXhhbXBsZS5jb20AAQIDAxUICgQWAAIBAhkBAhsDAh4B +FiEEEMCQTUVGKgCX5rDQ/7rSTgspvKUAAPl5AP9Npz90LxzrB97Qr2DrGwfG +wuYn4FSYwtuPfZHHeoIabwD/QEbvpQJ/NBb9EAZuow4Rirlt1yv19mmnF+j5 +8yUzhQjHXQRj2AiyEgorBgEEAZdVAQUBAQdARXAo30DmKcyUg6co7OUm0RNT +z9iqFbDBzA8A47JEt1MDAQgHAAD/XKK3lBm0SqMR558HLWdBrNG6NqKuqb5X +joCML987ZNgRD8J4BBgWCAAqBQJj2AiyCRD/utJOCym8pQIbDBYhBBDAkE1F +RioAl+aw0P+60k4LKbylAADRxgEAg7UfBDiDPp5LHcW9D+SgFHk6+GyEU4ev +VppQxdtxPvAA/34snHBX7Twnip1nMt7P4e2hDiw/hwQ7oqioOvc6jMkP +=Z8YJ +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/v2/keys_v5_test.go b/openpgp/v2/keys_v5_test.go new file mode 100644 index 000000000..e90e482df --- /dev/null +++ b/openpgp/v2/keys_v5_test.go @@ -0,0 +1,95 @@ +package v2 + +import ( + "bytes" + "strings" + "testing" +) + +var foreignKeys = []string{ + v5PrivKey, +} + +func TestReadPrivateForeignV5Key(t *testing.T) { + for _, str := range foreignKeys { + kring, err := ReadArmoredKeyRing(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + checkV5Key(t, kring[0]) + } +} + +func TestReadPrivateSerializeForeignV5Key(t *testing.T) { + for _, str := range foreignKeys { + el, err := ReadArmoredKeyRing(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + checkSerializeRead(t, el[0]) + } +} + +func checkV5Key(t *testing.T, ent *Entity) { + key := ent.PrimaryKey + if key.Version != 5 { + t.Errorf("wrong key version %d", key.Version) + } + if len(key.Fingerprint) != 32 { + t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) + } + signatures := ent.Revocations + for _, id := range ent.Identities { + signatures = append(signatures, id.SelfCertifications...) + } + for _, sig := range signatures { + if sig == nil { + continue + } + if sig.Packet.Version != 5 { + t.Errorf("wrong signature version %d", sig.Packet.Version) + } + fgptLen := len(sig.Packet.IssuerFingerprint) + if fgptLen != 32 { + t.Errorf("Wrong fingerprint length in signature: %d", fgptLen) + } + } +} + +func checkSerializeRead(t *testing.T, e *Entity) { + // Entity serialize + serializedEntity := bytes.NewBuffer(nil) + err := e.Serialize(serializedEntity) + if err != nil { + t.Fatal(err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV5Key(t, el[0]) + + // Without signing + serializedEntity = bytes.NewBuffer(nil) + err = e.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV5Key(t, el[0]) + + // Private + serializedEntity = bytes.NewBuffer(nil) + err = e.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV5Key(t, el[0]) +} diff --git a/openpgp/v2/keys_v6_test.go b/openpgp/v2/keys_v6_test.go new file mode 100644 index 000000000..cde778e2d --- /dev/null +++ b/openpgp/v2/keys_v6_test.go @@ -0,0 +1,226 @@ +package v2 + +import ( + "bytes" + "crypto" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +var foreignKeysV6 = []string{ + v6PrivKey, + v6ArgonSealedPrivKey, +} + +func TestReadPrivateForeignV6Key(t *testing.T) { + for _, str := range foreignKeysV6 { + kring, err := ReadArmoredKeyRing(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, kring[0]) + } +} + +func TestReadPrivateForeignV6KeyAndDecrypt(t *testing.T) { + password := []byte("correct horse battery staple") + kring, err := ReadArmoredKeyRing(strings.NewReader(v6ArgonSealedPrivKey)) + if err != nil { + t.Fatal(err) + } + key := kring[0] + if key.PrivateKey != nil && key.PrivateKey.Encrypted { + if err := key.PrivateKey.Decrypt(password); err != nil { + t.Fatal(err) + } + } + for _, sub := range key.Subkeys { + if sub.PrivateKey != nil && sub.PrivateKey.Encrypted { + if err := key.PrivateKey.Decrypt(password); err != nil { + t.Fatal(err) + } + } + } + checkV6Key(t, kring[0]) +} + +func TestReadPrivateEncryptedV6Key(t *testing.T) { + c := &packet.Config{V6Keys: true} + e, err := NewEntity("V6 Key Owner", "V6 Key", "v6@pm.me", c) + if err != nil { + t.Fatal(err) + } + password := []byte("test v6 key # password") + // Encrypt private key + if err = e.PrivateKey.Encrypt(password); err != nil { + t.Fatal(err) + } + // Encrypt subkeys + for _, sub := range e.Subkeys { + if err = sub.PrivateKey.Encrypt(password); err != nil { + t.Fatal(err) + } + } + // Serialize, Read + serializedEntity := bytes.NewBuffer(nil) + err = e.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + + // Decrypt + if el[0].PrivateKey == nil { + t.Fatal("No private key found") + } + if err = el[0].PrivateKey.Decrypt(password); err != nil { + t.Error(err) + } + + // Decrypt subkeys + for _, sub := range e.Subkeys { + if err = sub.PrivateKey.Decrypt(password); err != nil { + t.Error(err) + } + } + + checkV6Key(t, el[0]) +} + +func TestNewEntitySerializeV6Key(t *testing.T) { + c := &packet.Config{V6Keys: true} + e, err := NewEntity("V6 Key Owner", "V6 Key", "v6@pm.me", c) + if err != nil { + t.Fatal(err) + } + checkSerializeReadv6(t, e) +} + +func TestNewEntityV6Key(t *testing.T) { + c := &packet.Config{ + V6Keys: true, + } + e, err := NewEntity("V6 Key Owner", "V6 Key", "v6@pm.me", c) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, e) +} + +func checkV6Key(t *testing.T, ent *Entity) { + key := ent.PrimaryKey + if key.Version != 6 { + t.Errorf("wrong key version %d", key.Version) + } + if len(key.Fingerprint) != 32 { + t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) + } + signatures := ent.Revocations + for _, id := range ent.Identities { + signatures = append(signatures, id.SelfCertifications...) + } + for _, sig := range signatures { + if sig.Packet == nil { + continue + } + if sig.Packet.Version != 6 { + t.Errorf("wrong signature version %d", sig.Packet.Version) + } + fgptLen := len(sig.Packet.IssuerFingerprint) + if fgptLen != 32 { + t.Errorf("Wrong fingerprint length in signature: %d", fgptLen) + } + } +} + +func checkSerializeReadv6(t *testing.T, e *Entity) { + // Entity serialize + serializedEntity := bytes.NewBuffer(nil) + err := e.Serialize(serializedEntity) + if err != nil { + t.Fatal(err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, el[0]) + + // Without signing + serializedEntity = bytes.NewBuffer(nil) + err = e.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, el[0]) + + // Private + serializedEntity = bytes.NewBuffer(nil) + err = e.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + el, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + checkV6Key(t, el[0]) +} + +func TestNewEntityWithDefaultHashV6(t *testing.T) { + for _, hash := range hashes[:5] { + c := &packet.Config{ + V6Keys: true, + DefaultHash: hash, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if hash == crypto.SHA1 { + if err == nil { + t.Fatal("should fail on SHA1 key creation") + } + continue + } + var zeroTime time.Time + selfSig, err := entity.PrimarySelfSignature(zeroTime) + if err != nil { + t.Fatal("self-signature should be found") + } + prefs := selfSig.PreferredHash + if prefs == nil { + t.Fatal(err) + } + } +} + +func TestKeyGenerationHighSecurityLevel(t *testing.T) { + c := &packet.Config{ + V6Keys: true, + Algorithm: packet.PubKeyAlgoEd448, + DefaultHash: crypto.SHA256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if err != nil { + t.Fatal(err) + } + selfSig, err := entity.PrimarySelfSignature(time.Time{}) + if err != nil { + t.Fatal(err) + } + if !(selfSig.PreferredHash[0] == hashToHashId(crypto.SHA512)) { + t.Fatal("sha 512 should be the preferred option") + } + if selfSig.Hash != crypto.SHA512 { + t.Fatal("sha 512 should be used in self signatures") + } + +} diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go new file mode 100644 index 000000000..a8ed93493 --- /dev/null +++ b/openpgp/v2/read.go @@ -0,0 +1,800 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package openpgp implements high level operations on OpenPGP messages. +package v2 + +import ( + "bytes" + "crypto" + _ "crypto/sha256" + _ "crypto/sha512" + "hash" + "io" + "io/ioutil" + "strconv" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/packet" + _ "golang.org/x/crypto/sha3" +) + +// SignatureType is the armor type for a PGP signature. +var SignatureType = "PGP SIGNATURE" + +// readArmored reads an armored block with the given type. +func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { + block, err := armor.Decode(r) + if err != nil { + return + } + + if block.Type != expectedType { + return nil, errors.InvalidArgumentError("expected '" + expectedType + "', got: " + block.Type) + } + + return block.Body, nil +} + +// MessageDetails contains the result of parsing an OpenPGP encrypted and/or +// signed message. +type MessageDetails struct { + IsEncrypted bool // true if the message was encrypted. + EncryptedToKeyIds []uint64 // the list of recipient key ids. + IsSymmetricallyEncrypted bool // true if a passphrase could have decrypted the message. + DecryptedWith Key // the private key used to decrypt the message, if any. + DecryptedWithAlgorithm packet.CipherFunction // Stores the algorithm used to decrypt the message, if any. + IsSigned bool // true if the message is signed. + LiteralData *packet.LiteralData // the metadata of the contents + UnverifiedBody io.Reader // the contents of the message. + CheckRecipients bool // Indicates if the intended recipients should be checked + + SessionKey []byte // Caches the session key if the flag in packet.Config is set to true and a session key was present. + + // If IsSigned is true then the signature candidates will + // be verified as UnverifiedBody is read. The signature cannot be + // checked until the whole of UnverifiedBody is read so UnverifiedBody + // must be consumed until EOF before the data can be trusted. Even if a + // message isn't signed (or the signer is unknown) the data may contain + // an authentication code that is only checked once UnverifiedBody has + // been consumed. Once EOF has been seen, the following fields are + // valid. (An authentication code failure is reported as a + // SignatureError error when reading from UnverifiedBody.) + IsVerified bool // true if the signatures have been verified else false + SignatureCandidates []*SignatureCandidate // stores state for all signatures of this message + SignedBy *Key // the issuer key of the fist successfully verified signature, if any found. + Signature *packet.Signature // the first successfully verified signature, if any found. + // SignatureError is nil if one of the signatures in the message verifies successfully + // else it points to the last observed signature error. + // The error of each signature verification can be inspected by iterating trough + // SignatureCandidates. + SignatureError error + // SelectedCandidate points to the signature candidate the SignatureError error stems from or + // the selected successfully verified signature candidate. + SelectedCandidate *SignatureCandidate + + decrypted io.ReadCloser +} + +// A PromptFunction is used as a callback by functions that may need to decrypt +// a private key, or prompt for a passphrase. It is called with a list of +// acceptable, encrypted private keys and a boolean that indicates whether a +// passphrase is usable. It should either decrypt a private key or return a +// passphrase to try. If the decrypted private key or given passphrase isn't +// correct, the function will be called again, forever. Any error returned will +// be passed up. +type PromptFunction func(keys []Key, symmetric bool) ([]byte, error) + +// A keyEnvelopePair is used to store a private key with the envelope that +// contains a symmetric key, encrypted with that key. +type keyEnvelopePair struct { + key Key + encryptedKey *packet.EncryptedKey +} + +// ReadMessage parses an OpenPGP message that may be signed and/or encrypted. +// The given KeyRing should contain both public keys (for signature +// verification) and, possibly encrypted, private keys for decrypting. +// If config is nil, sensible defaults will be used. +func ReadMessage(r io.Reader, keyring KeyRing, prompt PromptFunction, config *packet.Config) (md *MessageDetails, err error) { + var p packet.Packet + + var symKeys []*packet.SymmetricKeyEncrypted + var pubKeys []keyEnvelopePair + // Integrity protected encrypted packet: SymmetricallyEncrypted or AEADEncrypted + var edp packet.EncryptedDataPacket + var packets packet.PacketReader + if config.StrictPacketSequence() { + packets = packet.NewCheckReader(r) + } else { + packets = packet.NewReader(r) + } + md = new(MessageDetails) + md.IsEncrypted = true + md.CheckRecipients = config.IntendedRecipients() + + // The message, if encrypted, starts with a number of packets + // containing an encrypted decryption key. The decryption key is either + // encrypted to a public key, or with a passphrase. This loop + // collects these packets. +ParsePackets: + for { + p, err = packets.Next() + if err != nil { + return nil, err + } + switch p := p.(type) { + case *packet.SymmetricKeyEncrypted: + // This packet contains the decryption key encrypted with a passphrase. + md.IsSymmetricallyEncrypted = true + symKeys = append(symKeys, p) + case *packet.EncryptedKey: + // This packet contains the decryption key encrypted to a public key. + md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) + switch p.Algo { + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, + packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + break + default: + continue + } + if keyring != nil { + unverifiedEntities := keyring.EntitiesById(p.KeyId) + for _, unverifiedEntity := range unverifiedEntities { + // Do not check key expiration to allow decryption of old messages + keys := unverifiedEntity.DecryptionKeys(p.KeyId, time.Time{}) + for _, key := range keys { + pubKeys = append(pubKeys, keyEnvelopePair{key, p}) + } + } + } + case *packet.SymmetricallyEncrypted: + if !p.IntegrityProtected && !config.AllowUnauthenticatedMessages() { + return nil, errors.UnsupportedError("message is not integrity protected") + } + edp = p + if p.Version == 2 { // SEIPD v2 packet stores the decryption algorithm + md.DecryptedWithAlgorithm = p.Cipher + } + break ParsePackets + case *packet.AEADEncrypted: + edp = p + break ParsePackets + case *packet.Compressed, *packet.LiteralData, *packet.OnePassSignature, *packet.Signature: + // This message isn't encrypted. + if len(symKeys) != 0 || len(pubKeys) != 0 { + return nil, errors.StructuralError("key material not followed by encrypted message") + } + packets.Unread(p) + md.IsEncrypted = false + return readSignedMessage(packets, md, keyring, config) + } + } + + var candidates []Key + var decrypted io.ReadCloser + + // Now that we have the list of encrypted keys we need to decrypt at + // least one of them or, if we cannot, we need to call the prompt + // function so that it can decrypt a key or give us a passphrase. +FindKey: + for { + // See if any of the keys already have a private key available + candidates = candidates[:0] + candidateFingerprints := make(map[string]bool) + + for _, pk := range pubKeys { + if pk.key.PrivateKey == nil { + continue + } + if !pk.key.PrivateKey.Encrypted { + if len(pk.encryptedKey.Key) == 0 { + errDec := pk.encryptedKey.Decrypt(pk.key.PrivateKey, config) + if errDec != nil { + continue + } + } + // Try to decrypt symmetrically encrypted + decrypted, err = edp.Decrypt(pk.encryptedKey.CipherFunc, pk.encryptedKey.Key) + if err != nil && err != errors.ErrKeyIncorrect { + return nil, err + } + if decrypted != nil { + md.DecryptedWith = pk.key + if md.DecryptedWithAlgorithm == 0 { // if no SEIPD v2 packet, key packet stores the decryption algorithm + md.DecryptedWithAlgorithm = pk.encryptedKey.CipherFunc + } + if config.RetrieveSessionKey() { + md.SessionKey = pk.encryptedKey.Key + } + break FindKey + } + } else { + fpr := string(pk.key.PublicKey.Fingerprint[:]) + if v := candidateFingerprints[fpr]; v { + continue + } + candidates = append(candidates, pk.key) + candidateFingerprints[fpr] = true + } + } + + if len(candidates) == 0 && len(symKeys) == 0 { + return nil, errors.ErrKeyIncorrect + } + + if prompt == nil { + return nil, errors.ErrKeyIncorrect + } + + passphrase, err := prompt(candidates, len(symKeys) != 0) + if err != nil { + return nil, err + } + + // Try the symmetric passphrase first + if len(symKeys) != 0 && passphrase != nil { + for _, s := range symKeys { + key, cipherFunc, err := s.Decrypt(passphrase) + // In v4, on wrong passphrase, session key decryption is very likely to result in an invalid cipherFunc: + // only for < 5% of cases we will proceed to decrypt the data + if err == nil { + decrypted, err = edp.Decrypt(cipherFunc, key) + if err != nil { + return nil, err + } + if decrypted != nil { + if md.DecryptedWithAlgorithm == 0 { // if no SEIPD v2 packet, key packet stores the decryption algorithm + md.DecryptedWithAlgorithm = cipherFunc + } + if config.RetrieveSessionKey() { + md.SessionKey = key + } + break FindKey + } + } + } + } + } + + md.decrypted = decrypted + if err := packets.Push(decrypted); err != nil { + return nil, err + } + mdFinal, sensitiveParsingErr := readSignedMessage(packets, md, keyring, config) + if sensitiveParsingErr != nil { + return nil, errors.StructuralError("parsing error") + } + return mdFinal, nil +} + +// SignatureCandidate keeps state about a signature that can be potentially verified. +type SignatureCandidate struct { + OPSVersion int + SigType packet.SignatureType + HashAlgorithm crypto.Hash + PubKeyAlgo packet.PublicKeyAlgorithm + IssuerKeyId uint64 + IssuerFingerprint []byte + Salt []byte // v6 only + + SignedByEntity *Entity + SignedBy *Key // the key of the signer, if available. (OPS) + SignatureError error // nil if the signature is valid or not checked. + CorrespondingSig *packet.Signature // the candidate's signature packet + Hash, WrappedHash hash.Hash // hashes for this signature candidate (OPS) +} + +func newSignatureCandidate(ops *packet.OnePassSignature) (sigCandidate *SignatureCandidate) { + sigCandidate = &SignatureCandidate{ + OPSVersion: ops.Version, + SigType: ops.SigType, + HashAlgorithm: ops.Hash, + PubKeyAlgo: ops.PubKeyAlgo, + IssuerKeyId: ops.KeyId, + Salt: ops.Salt, + IssuerFingerprint: ops.KeyFingerprint, + } + sigCandidate.Hash, sigCandidate.WrappedHash, sigCandidate.SignatureError = hashForSignature( + sigCandidate.HashAlgorithm, + sigCandidate.SigType, + sigCandidate.Salt, + ) + return +} + +func newSignatureCandidateFromSignature(sig *packet.Signature) (sigCandidate *SignatureCandidate) { + sigCandidate = &SignatureCandidate{ + SigType: sig.SigType, + HashAlgorithm: sig.Hash, + PubKeyAlgo: sig.PubKeyAlgo, + IssuerKeyId: *sig.IssuerKeyId, + IssuerFingerprint: sig.IssuerFingerprint, + Salt: sig.Salt(), + } + sigCandidate.OPSVersion = 3 + if sig.Version == 6 { + sigCandidate.OPSVersion = sig.Version + } + sigCandidate.Hash, sigCandidate.WrappedHash, sigCandidate.SignatureError = hashForSignature( + sigCandidate.HashAlgorithm, + sigCandidate.SigType, + sigCandidate.Salt, + ) + sigCandidate.CorrespondingSig = sig + return +} + +func (sc *SignatureCandidate) validate() bool { + correspondingSig := sc.CorrespondingSig + invalidV3 := sc.OPSVersion == 3 && correspondingSig.Version == 6 + invalidV6 := (sc.OPSVersion == 6 && correspondingSig.Version != 6) || + (sc.OPSVersion == 6 && !bytes.Equal(sc.IssuerFingerprint, correspondingSig.IssuerFingerprint)) || + (sc.OPSVersion == 6 && !bytes.Equal(sc.Salt, correspondingSig.Salt())) + return correspondingSig != nil && + sc.SigType == correspondingSig.SigType && + sc.HashAlgorithm == correspondingSig.Hash && + sc.PubKeyAlgo == correspondingSig.PubKeyAlgo && + sc.IssuerKeyId == *correspondingSig.IssuerKeyId && + !invalidV3 && + !invalidV6 +} + +// readSignedMessage reads a possibly signed message if mdin is non-zero then +// that structure is updated and returned. Otherwise a fresh MessageDetails is +// used. +func readSignedMessage(packets packet.PacketReader, mdin *MessageDetails, keyring KeyRing, config *packet.Config) (md *MessageDetails, err error) { + if mdin == nil { + mdin = new(MessageDetails) + } + md = mdin + + var p packet.Packet + var prevLast bool +FindLiteralData: + for { + p, err = packets.Next() + if err != nil { + return nil, err + } + switch p := p.(type) { + case *packet.Compressed: + if err := packets.Push(p.Body); err != nil { + return nil, err + } + case *packet.OnePassSignature: + if prevLast { + return nil, errors.UnsupportedError("nested signature packets") + } + + if p.IsLast { + prevLast = true + } + + sigCandidate := newSignatureCandidate(p) + md.IsSigned = true + if keyring != nil { + keys := keyring.EntitiesById(p.KeyId) + if len(keys) > 0 { + sigCandidate.SignedByEntity = keys[0] + } + } + // If a message contains more than one one-pass signature, then the Signature packets bracket the message + md.SignatureCandidates = append([]*SignatureCandidate{sigCandidate}, md.SignatureCandidates...) + case *packet.Signature: + // Old style signature i.e., sig | literal + sigCandidate := newSignatureCandidateFromSignature(p) + md.IsSigned = true + if keyring != nil { + keys := keyring.EntitiesById(sigCandidate.IssuerKeyId) + if len(keys) > 0 { + sigCandidate.SignedByEntity = keys[0] + } + } + md.SignatureCandidates = append([]*SignatureCandidate{sigCandidate}, md.SignatureCandidates...) + case *packet.LiteralData: + md.LiteralData = p + break FindLiteralData + case *packet.EncryptedKey, + *packet.SymmetricKeyEncrypted, + *packet.AEADEncrypted, + *packet.SymmetricallyEncrypted: + return nil, errors.UnsupportedError("cannot read signed message with encrypted data") + } + } + + if md.IsSigned { + md.UnverifiedBody = &signatureCheckReader{packets, md, config, md.LiteralData.Body} + } else { + md.UnverifiedBody = &checkReader{md, packets, false} + } + + return md, nil +} + +func wrapHashForSignature(hashFunc hash.Hash, sigType packet.SignatureType) (hash.Hash, error) { + switch sigType { + case packet.SigTypeBinary: + return hashFunc, nil + case packet.SigTypeText: + return NewCanonicalTextHash(hashFunc), nil + } + return nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType))) +} + +// hashForSignature returns a pair of hashes that can be used to verify a +// signature. The signature may specify that the contents of the signed message +// should be preprocessed (i.e. to normalize line endings). Thus this function +// returns two hashes. The first, directHash, will feed directly into the signature algorithm. +// The second, wrappedHash, should be used to hash the message itself and performs any needed preprocessing. +func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType, sigSalt []byte) (directHash hash.Hash, wrappedHash hash.Hash, err error) { + if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok { + return nil, nil, errors.UnsupportedError("unsupported hash function") + } + if !hashFunc.Available() { + return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc))) + } + h := hashFunc.New() + if sigSalt != nil { + h.Write(sigSalt) + } + wrappedHash, err = wrapHashForSignature(h, sigType) + if err != nil { + return nil, nil, err + } + switch sigType { + case packet.SigTypeBinary: + return h, wrappedHash, nil + case packet.SigTypeText: + return h, wrappedHash, nil + } + return nil, nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType))) +} + +// checkReader wraps an io.Reader from a LiteralData packet. When it sees EOF +// it closes the ReadCloser from any SymmetricallyEncrypted packet to trigger +// MDC checks. +type checkReader struct { + md *MessageDetails + packets packet.PacketReader + checked bool +} + +func (cr *checkReader) Read(buf []byte) (int, error) { + n, sensitiveParsingError := cr.md.LiteralData.Body.Read(buf) + if sensitiveParsingError == io.EOF { + if cr.checked { + return n, io.EOF + } + for { + _, err := cr.packets.Next() + if err == io.EOF { + break + } + if err != nil { + return n, err + } + } + if cr.md.decrypted != nil { + if mdcErr := cr.md.decrypted.Close(); mdcErr != nil { + return n, mdcErr + } + } + cr.checked = true + return n, io.EOF + } + if sensitiveParsingError != nil { + return n, errors.StructuralError("parsing error") + } + return n, nil +} + +// signatureCheckReader wraps an io.Reader from a LiteralData packet and hashes +// the data as it is read. When it sees an EOF from the underlying io.Reader +// it parses and checks a trailing Signature packet and triggers any MDC checks. +type signatureCheckReader struct { + packets packet.PacketReader + md *MessageDetails + config *packet.Config + data io.Reader +} + +func (scr *signatureCheckReader) Read(buf []byte) (int, error) { + n, sensitiveParsingError := scr.data.Read(buf) + + for _, candidate := range scr.md.SignatureCandidates { + if candidate.SignatureError == nil && candidate.SignedByEntity != nil { + candidate.WrappedHash.Write(buf[:n]) + } + } + + if sensitiveParsingError == io.EOF { + var signatures []*packet.Signature + + // Read all signature packets. + + for { + p, err := scr.packets.Next() + if err == io.EOF { + break + } + if err != nil { + return n, errors.StructuralError("parsing error") + } + if sig, ok := p.(*packet.Signature); ok { + if sig.Version == 5 && scr.md.LiteralData != nil && (sig.SigType == 0x00 || sig.SigType == 0x01) { + sig.Metadata = scr.md.LiteralData + } + signatures = append(signatures, sig) + } + } + numberOfOpsSignatures := 0 + for _, candidate := range scr.md.SignatureCandidates { + if candidate.CorrespondingSig == nil { + numberOfOpsSignatures++ + } + } + + if len(signatures) != numberOfOpsSignatures { + // Cannot handle this case yet with no information about invalid packets, should fail. + // This case can happen if a known OPS version is used but an unknown signature version. + noMatchError := errors.StructuralError("number of ops signature candidates does not match the number of signature packets") + for _, candidate := range scr.md.SignatureCandidates { + candidate.SignatureError = noMatchError + } + } else { + var sigIndex int + // Verify all signature candidates. + for _, candidate := range scr.md.SignatureCandidates { + if candidate.CorrespondingSig == nil { + candidate.CorrespondingSig = signatures[sigIndex] + sigIndex++ + } + if !candidate.validate() { + candidate.SignatureError = errors.StructuralError("signature does not match the expected ops data") + } + if candidate.SignatureError == nil { + sig := candidate.CorrespondingSig + if candidate.SignedByEntity == nil { + candidate.SignatureError = errors.ErrUnknownIssuer + scr.md.SignatureError = candidate.SignatureError + } else { + // Verify and retrieve signing key at signature creation time + key, err := candidate.SignedByEntity.signingKeyByIdUsage(sig.CreationTime, candidate.IssuerKeyId, packet.KeyFlagSign, scr.config) + if err != nil { + candidate.SignatureError = err + continue + } else { + candidate.SignedBy = &key + } + signatureError := key.PublicKey.VerifySignature(candidate.Hash, sig) + if signatureError == nil { + signatureError = checkSignatureDetails(&key, sig, scr.config) + } + if !scr.md.IsSymmetricallyEncrypted && len(sig.IntendedRecipients) > 0 && scr.md.CheckRecipients && signatureError == nil { + if !scr.md.IsEncrypted { + signatureError = errors.SignatureError("intended recipients in non-encrypted message") + } else { + // Check signature matches one of the recipients + signatureError = checkIntendedRecipientsMatch(&scr.md.DecryptedWith, sig) + } + } + candidate.SignatureError = signatureError + } + } + } + } + + // Check if there is a valid candidate. + for _, candidate := range scr.md.SignatureCandidates { + if candidate.SignedBy == nil { + // Ignore candidates that have no matching key + continue + } + // md.SignatureError points to the last candidate with a key match, if + // all signature verifications have failed. + scr.md.SignatureError = candidate.SignatureError + scr.md.SelectedCandidate = candidate + if candidate.SignatureError == nil { + // There is a valid signature. + scr.md.Signature = candidate.CorrespondingSig + scr.md.SignedBy = candidate.SignedBy + break + } + } + + if len(scr.md.SignatureCandidates) == 0 { + scr.md.SignatureError = errors.StructuralError("no signature found") + } + + if len(scr.md.SignatureCandidates) > 0 && scr.md.SelectedCandidate == nil { + // No candidate with a matching key present. + // Just point to the last candidate in this case. + candidate := scr.md.SignatureCandidates[len(scr.md.SignatureCandidates)-1] + scr.md.SignatureError = candidate.SignatureError + scr.md.SelectedCandidate = candidate + } + + if scr.md.SignatureError == nil && scr.md.Signature == nil { + scr.md.SignatureError = errors.StructuralError("no matching signature found") + } + + scr.md.IsVerified = true + + for { + _, err := scr.packets.Next() + if err == io.EOF { + break + } + if err != nil { + return 0, errors.StructuralError("parsing error") + } + + } + + // The SymmetricallyEncrypted packet, if any, might have an + // unsigned hash of its own. In order to check this we need to + // close that Reader. + if scr.md.decrypted != nil { + mdcErr := scr.md.decrypted.Close() + if mdcErr != nil { + return n, mdcErr + } + } + return n, io.EOF + } + + if sensitiveParsingError != nil { + return n, errors.StructuralError("parsing error") + } + + return n, nil +} + +// VerifyDetachedSignature takes a signed file and a detached signature and +// returns the signature packet and the entity the signature was signed by, +// if any, and a possible signature verification error. +// If the signer isn't known, ErrUnknownIssuer is returned. +func VerifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { + return verifyDetachedSignature(keyring, signed, signature, config) +} + +// VerifyDetachedSignatureReader takes a signed file and a detached signature and +// returns message details struct similar to the ReadMessage function. +// Once all data is read from md.UnverifiedBody the detached signature is verified. +// If a verification error occurs it is stored in md.SignatureError +// If the signer isn't known, ErrUnknownIssuer is returned. +// If expectedHashes or expectedSaltedHashes is not nil, the method checks +// if they match the signatures metadata or else return an error +func VerifyDetachedSignatureReader(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (md *MessageDetails, err error) { + return verifyDetachedSignatureReader(keyring, signed, signature, config) +} + +// VerifyArmoredDetachedSignature performs the same actions as +// VerifyDetachedSignature but expects the signature to be armored. +func VerifyArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { + body, err := readArmored(signature, SignatureType) + if err != nil { + return + } + + return VerifyDetachedSignature(keyring, signed, body, config) +} + +func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { + md, err := verifyDetachedSignatureReader(keyring, signed, signature, config) + if err != nil { + return nil, nil, err + } + _, err = io.Copy(ioutil.Discard, md.UnverifiedBody) + if err != nil { + return nil, nil, err + } + if md.SignatureError != nil { + return nil, nil, md.SignatureError + } + return md.Signature, md.SignedBy.Entity, nil +} + +func verifyDetachedSignatureReader(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (md *MessageDetails, err error) { + var p packet.Packet + md = &MessageDetails{ + IsEncrypted: false, + CheckRecipients: false, + IsSigned: true, + } + + packets := packet.NewReader(signature) + for { + p, err = packets.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + sig, ok := p.(*packet.Signature) + if !ok { + continue + } + if sig.IssuerKeyId == nil { + return nil, errors.StructuralError("signature doesn't have an issuer") + } + candidate := newSignatureCandidateFromSignature(sig) + md.SignatureCandidates = append(md.SignatureCandidates, candidate) + + keys := keyring.EntitiesById(candidate.IssuerKeyId) + if len(keys) > 0 { + candidate.SignedByEntity = keys[0] + } + } + + if len(md.SignatureCandidates) == 0 { + return nil, errors.ErrUnknownIssuer + } + md.UnverifiedBody = &signatureCheckReader{packets, md, config, signed} + return md, nil +} + +// checkSignatureDetails verifies the metadata of the signature. +// Checks the following: +// - Hash function should not be invalid. +// - Verification key must be older than the signature creation time. +// - Check signature notations. +// - Signature is not expired. +func checkSignatureDetails(verifiedKey *Key, signature *packet.Signature, config *packet.Config) error { + var collectedErrors []error + now := config.Now() + + if config.RejectMessageHashAlgorithm(signature.Hash) { + return errors.SignatureError("insecure message hash algorithm: " + signature.Hash.String()) + } + + if verifiedKey.PublicKey.CreationTime.Unix() > signature.CreationTime.Unix() { + collectedErrors = append(collectedErrors, errors.ErrSignatureOlderThanKey) + } + + sigsToCheck := []*packet.Signature{signature, verifiedKey.PrimarySelfSignature} + + if !verifiedKey.IsPrimary() { + sigsToCheck = append(sigsToCheck, verifiedKey.SelfSignature, verifiedKey.SelfSignature.EmbeddedSignature) + } + for _, sig := range sigsToCheck { + for _, notation := range sig.Notations { + if notation.IsCritical && !config.KnownNotation(notation.Name) { + return errors.SignatureError("unknown critical notation: " + notation.Name) + } + } + } + + if signature.SigExpired(now) { + return errors.ErrSignatureExpired + } + + if len(collectedErrors) > 0 { + // TODO: Is there a better priority for errors? + return collectedErrors[len(collectedErrors)-1] + } + return nil +} + +// checkIntendedRecipientsMatch checks if the fingerprint of the primary key matching the decryption key +// is found in the signature's intended recipients list. +func checkIntendedRecipientsMatch(decryptionKey *Key, sig *packet.Signature) error { + match := false + for _, recipient := range sig.IntendedRecipients { + if bytes.Equal(recipient.Fingerprint, decryptionKey.Entity.PrimaryKey.Fingerprint) { + match = true + break + } + } + if !match { + return errors.SignatureError("intended recipients in the signature does not match the decryption key") + } + return nil +} diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go new file mode 100644 index 000000000..024ae608c --- /dev/null +++ b/openpgp/v2/read_test.go @@ -0,0 +1,1002 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + "bytes" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func readerFromHex(s string) io.Reader { + data, err := hex.DecodeString(s) + if err != nil { + panic("readerFromHex: bad input") + } + return bytes.NewBuffer(data) +} + +func TestReadKeyRing(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + if err != nil { + t.Error(err) + return + } + if len(kring) != 2 || uint32(kring[0].PrimaryKey.KeyId) != 0xC20C31BB || uint32(kring[1].PrimaryKey.KeyId) != 0x1E35246B { + t.Errorf("bad keyring: %#v", kring) + } +} + +func TestRereadKeyRing(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + if err != nil { + t.Errorf("error in initial parse: %s", err) + return + } + out := new(bytes.Buffer) + err = kring[0].Serialize(out) + if err != nil { + t.Errorf("error in serialization: %s", err) + return + } + kring, err = ReadKeyRing(out) + if err != nil { + t.Errorf("error in second parse: %s", err) + return + } + + if len(kring) != 1 || uint32(kring[0].PrimaryKey.KeyId) != 0xC20C31BB { + t.Errorf("bad keyring: %#v", kring) + } +} + +func TestReadPrivateKeyRing(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + if err != nil { + t.Error(err) + return + } + if len(kring) != 2 || uint32(kring[0].PrimaryKey.KeyId) != 0xC20C31BB || uint32(kring[1].PrimaryKey.KeyId) != 0x1E35246B || kring[0].PrimaryKey == nil { + t.Errorf("bad keyring: %#v", kring) + } +} + +func TestReadDSAKey(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(dsaTestKeyHex)) + if err != nil { + t.Error(err) + return + } + if len(kring) != 1 || uint32(kring[0].PrimaryKey.KeyId) != 0x0CCC0360 { + t.Errorf("bad parse: %#v", kring) + } +} + +func TestReadP256Key(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(p256TestKeyHex)) + if err != nil { + t.Error(err) + return + } + if len(kring) != 1 || uint32(kring[0].PrimaryKey.KeyId) != 0x5918513E { + t.Errorf("bad parse: %#v", kring) + } +} + +func TestDSAHashTruncatation(t *testing.T) { + // dsaKeyWithSHA512 was generated with GnuPG and --cert-digest-algo + // SHA512 in order to require DSA hash truncation to verify correctly. + _, err := ReadKeyRing(readerFromHex(dsaKeyWithSHA512)) + if err != nil { + t.Error(err) + } +} + +func TestGetKeyById(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + + keys := kring.KeysById(0xa34d7e18c20c31bb) + if len(keys) != 1 || keys[0].Entity != kring[0] { + t.Errorf("bad result for 0xa34d7e18c20c31bb: %#v", keys) + } + + keys = kring.KeysById(0xfd94408d4543314f) + if len(keys) != 1 || keys[0].Entity != kring[0] { + t.Errorf("bad result for 0xa34d7e18c20c31bb: %#v", keys) + } +} + +func checkSignedMessage(t *testing.T, signedHex, expected string) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + + md, err := ReadMessage(readerFromHex(signedHex), kring, nil, &allowAllAlgorithmsConfig) + if err != nil { + t.Error(err) + return + } + + if len(md.SignatureCandidates) < 1 { + t.Errorf("bad MessageDetails: %#v", md) + } + + if !md.IsSigned || md.SignatureCandidates[0].IssuerKeyId != 0xa34d7e18c20c31bb || md.SignatureCandidates[0].SignedByEntity == nil || md.IsEncrypted || md.IsSymmetricallyEncrypted || len(md.EncryptedToKeyIds) != 0 || md.DecryptedWith.Entity != nil { + t.Errorf("bad MessageDetails: %#v", md) + } + + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading UnverifiedBody: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if string(contents) != expected { + t.Errorf("bad UnverifiedBody got:%s want:%s", string(contents), expected) + } + if md.SignatureError != nil || md.Signature == nil { + t.Errorf("failed to validate: %s", md.SignatureError) + } +} + +func TestSignedMessage(t *testing.T) { + checkSignedMessage(t, signedMessageHex, signedInput) +} + +func TestTextSignedMessage(t *testing.T) { + checkSignedMessage(t, signedTextMessageHex, signedTextInput) +} + +// The reader should detect "compressed quines", which are compressed +// packets that expand into themselves and cause an infinite recursive +// parsing loop. +// The packet in this test case comes from Taylor R. Campbell at +// http://mumble.net/~campbell/misc/pgp-quine/ +func TestCampbellQuine(t *testing.T) { + md, err := ReadMessage(readerFromHex(campbellQuine), nil, nil, nil) + if md != nil { + t.Errorf("Reading a compressed quine should not return any data: %#v", md) + } + structural, ok := err.(errors.StructuralError) + if !ok { + t.Fatalf("Unexpected class of error: %T", err) + } + if !strings.Contains(string(structural), "too many layers of packets") { + t.Fatalf("Unexpected error: %s", err) + } +} + +func TestSignedEncryptedMessage(t *testing.T) { + var signedEncryptedMessageTests = []struct { + keyRingHex string + messageHex string + signedByKeyId uint64 + encryptedToKeyId uint64 + verifiedSigHex string + unverifiedSigHex string + }{ + { + testKeys1And2PrivateHex, + signedEncryptedMessageHex, + 0xa34d7e18c20c31bb, + 0x2a67d68660df41c7, + verifiedSignatureEncryptedMessageHex, + unverifiedSignatureEncryptedMessageHex, + }, + { + dsaElGamalTestKeysHex, + signedEncryptedMessage2Hex, + 0x33af447ccd759b09, + 0xcf6a7abcd43e3673, + signatureEncryptedMessage2Hex, + "", + }, + } + for i, test := range signedEncryptedMessageTests { + expected := "Signed and encrypted message\n" + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + prompt := func(keys []Key, symmetric bool) ([]byte, error) { + if symmetric { + t.Errorf("prompt: message was marked as symmetrically encrypted") + return nil, errors.ErrKeyIncorrect + } + + if len(keys) == 0 { + t.Error("prompt: no keys requested") + return nil, errors.ErrKeyIncorrect + } + + err := keys[0].PrivateKey.Decrypt([]byte("passphrase")) + if err != nil { + t.Errorf("prompt: error decrypting key: %s", err) + return nil, errors.ErrKeyIncorrect + } + + return nil, nil + } + + md, err := ReadMessage(readerFromHex(test.messageHex), kring, prompt, &allowAllAlgorithmsConfig) + if err != nil { + t.Errorf("#%d: error reading message: %s", i, err) + return + } + + if len(md.SignatureCandidates) < 1 { + t.Errorf("#%d: bad MessageDetails: %#v", i, md) + } + + if !md.IsSigned || md.SignatureCandidates[0].IssuerKeyId != test.signedByKeyId || md.SignatureCandidates[0].SignedByEntity == nil || !md.IsEncrypted || md.IsSymmetricallyEncrypted || len(md.EncryptedToKeyIds) == 0 || md.EncryptedToKeyIds[0] != test.encryptedToKeyId { + t.Errorf("#%d: bad MessageDetails: %#v", i, md) + } + + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("#%d: error reading UnverifiedBody: %s", i, err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if string(contents) != expected { + t.Errorf("#%d: bad UnverifiedBody got:%s want:%s", i, string(contents), expected) + } + + if md.SignatureError != nil || md.Signature == nil { + t.Errorf("#%d: failed to validate: %s", i, md.SignatureError) + } + + if test.verifiedSigHex != "" { + var sig bytes.Buffer + err = md.Signature.Serialize(&sig) + if err != nil { + t.Errorf("#%d: error serializing verified signature: %s", i, err) + } + + sigData, err := io.ReadAll(&sig) + if err != nil { + t.Errorf("#%d: error reading verified signature: %s", i, err) + } + + if hex.EncodeToString(sigData) != test.verifiedSigHex { + t.Errorf("#%d: verified signature does not match: %s, %s", i, hex.EncodeToString(sigData), test.verifiedSigHex) + } + } + } +} + +func TestUnspecifiedRecipient(t *testing.T) { + expected := "Recipient unspecified\n" + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + + md, err := ReadMessage(readerFromHex(recipientUnspecifiedHex), kring, nil, nil) + if err != nil { + t.Errorf("error reading message: %s", err) + return + } + + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading UnverifiedBody: %s", err) + } + if string(contents) != expected { + t.Errorf("bad UnverifiedBody got:%s want:%s", string(contents), expected) + } +} + +func TestSymmetricallyEncrypted(t *testing.T) { + firstTimeCalled := true + + prompt := func(keys []Key, symmetric bool) ([]byte, error) { + if len(keys) != 0 { + t.Errorf("prompt: len(keys) = %d (want 0)", len(keys)) + } + + if !symmetric { + t.Errorf("symmetric is not set") + } + + if firstTimeCalled { + firstTimeCalled = false + return []byte("wrongpassword"), nil + } + + return []byte("password"), nil + } + + md, err := ReadMessage(readerFromHex(symmetricallyEncryptedCompressedHex), nil, prompt, nil) + if err != nil { + t.Errorf("ReadMessage: %s", err) + return + } + + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("ReadAll: %s", err) + } + + expectedCreationTime := uint32(1555107469) + if md.LiteralData.Time != expectedCreationTime { + t.Errorf("LiteralData.Time is %d, want %d", md.LiteralData.Time, expectedCreationTime) + } + + const expected = "Symmetrically encrypted.\r\n" + if string(contents) != expected { + t.Errorf("contents got: %s want: %s", string(contents), expected) + } +} + +func testDetachedSignature(t *testing.T, kring KeyRing, signature io.Reader, sigInput, tag string, expectedSignerKeyId uint64) { + signed := bytes.NewBufferString(sigInput) + _, signer, err := VerifyDetachedSignature(kring, signed, signature, &allowAllAlgorithmsConfig) + if err != nil { + t.Errorf("%s: signature error: %s", tag, err) + return + } + if signer == nil { + t.Errorf("%s: signer is nil", tag) + return + } + if signer.PrimaryKey.KeyId != expectedSignerKeyId { + t.Errorf("%s: wrong signer: got %x, expected %x", tag, signer.PrimaryKey.KeyId, expectedSignerKeyId) + } +} + +func TestDetachedSignature(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + testDetachedSignature(t, kring, readerFromHex(detachedSignatureHex), signedInput, "binary", testKey1KeyId) + testDetachedSignature(t, kring, readerFromHex(detachedSignatureTextHex), signedInput, "text", testKey1KeyId) + + incorrectSignedInput := signedInput + "X" + config := &packet.Config{} + _, _, err := VerifyDetachedSignature(kring, bytes.NewBufferString(incorrectSignedInput), readerFromHex(detachedSignatureHex), config) + if err == nil { + t.Fatal("CheckDetachedSignature returned without error for bad signature") + } + if err == errors.ErrUnknownIssuer { + t.Fatal("CheckDetachedSignature returned ErrUnknownIssuer when the signer was known, but the signature invalid") + } +} + +func TestDetachedSignatureDSA(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(dsaTestKeyHex)) + testDetachedSignature(t, kring, readerFromHex(detachedSignatureDSAHex), signedInput, "binary", testKey3KeyId) +} + +func TestMultipleSignaturePacketsDSA(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(dsaTestKeyHex)) + testDetachedSignature(t, kring, readerFromHex(missingHashFunctionHex+detachedSignatureDSAHex), signedInput, "binary", testKey3KeyId) +} + +func TestDetachedSignatureP256(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(p256TestKeyHex)) + testDetachedSignature(t, kring, readerFromHex(detachedSignatureP256Hex), signedInput, "binary", testKeyP256KeyId) +} + +func testHashFunctionError(t *testing.T, signatureHex string) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + config := &packet.Config{} + _, _, err := VerifyDetachedSignature(kring, nil, readerFromHex(signatureHex), config) + if err == nil { + t.Fatal("Packet with bad hash type was correctly parsed") + } + if err != errors.ErrUnknownIssuer { + t.Fatalf("Unexpected class of error: %s", err) + } +} + +func TestUnknownHashFunction(t *testing.T) { + // unknownHashFunctionHex contains a signature packet with hash function type + // 153 (which isn't a real hash function id). Since that's the only signature + // packet we don't find any suitable packets and end up with ErrUnknownIssuer. + testHashFunctionError(t, unknownHashFunctionHex) +} + +func TestMissingHashFunction(t *testing.T) { + // missingHashFunctionHex contains a signature packet that uses RIPEMD160, + // which isn't compiled in. Since that's the only signature packet we don't + // find any suitable packets and end up with ErrUnknownIssuer. + testHashFunctionError(t, missingHashFunctionHex) +} + +func TestRSASignatureBadMPILength(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2Hex)) + config := &packet.Config{} + _, _, err := VerifyDetachedSignature(kring, nil, readerFromHex(rsaSignatureBadMPIlength), config) + if err == nil { + t.Fatal("RSA Signature with malformed MPI was correctly parsed") + } +} + +func TestDetachedSignatureExpiredCrossSig(t *testing.T) { + kring, _ := ReadArmoredKeyRing(bytes.NewBufferString(keyWithExpiredCrossSig)) + config := &packet.Config{} + _, _, err := VerifyArmoredDetachedSignature(kring, bytes.NewBufferString("Hello World :)"), bytes.NewBufferString(sigFromKeyWithExpiredCrossSig), config) + if err == nil { + t.Fatal("Signature from key with expired subkey binding embedded signature was accepted") + } +} + +func TestSignatureUnknownNotation(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(criticalNotationSigner)) + if err != nil { + t.Error(err) + } + raw, err := armor.Decode(strings.NewReader(signedMessageWithCriticalNotation)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(raw.Body, el, nil, &allowAllAlgorithmsConfig) + if err != nil { + t.Error(err) + return + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + const expectedErr string = "openpgp: invalid signature: unknown critical notation: test@example.com" + if md.SignatureError == nil || md.SignatureError.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, md.SignatureError) + } +} + +func TestSignatureKnownNotation(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(criticalNotationSigner)) + if err != nil { + t.Error(err) + } + raw, err := armor.Decode(strings.NewReader(signedMessageWithCriticalNotation)) + if err != nil { + t.Error(err) + return + } + config := allowAllAlgorithmsConfig + config.KnownNotations = map[string]bool{ + "test@example.com": true, + } + md, err := ReadMessage(raw.Body, el, nil, &config) + if err != nil { + t.Error(err) + return + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Error(md.SignatureError) + return + } +} + +func TestReadingArmoredPrivateKey(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKeyBlock)) + if err != nil { + t.Error(err) + } + if len(el) != 1 { + t.Errorf("got %d entities, wanted 1\n", len(el)) + } +} + +func TestReadingArmoredPublicKey(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(e2ePublicKey)) + if err != nil { + t.Error(err) + } + if len(el) != 1 { + t.Errorf("didn't get a valid entity") + } +} + +func TestNoArmoredData(t *testing.T) { + _, err := ReadArmoredKeyRing(bytes.NewBufferString("foo")) + if _, ok := err.(errors.InvalidArgumentError); !ok { + t.Errorf("error was not an InvalidArgumentError: %s", err) + } +} + +func testReadMessageError(t *testing.T, messageHex string) { + buf, err := hex.DecodeString(messageHex) + if err != nil { + t.Errorf("hex.DecodeString(): %v", err) + } + + kr, err := ReadKeyRing(new(bytes.Buffer)) + if err != nil { + t.Errorf("ReadKeyring(): %v", err) + } + + _, err = ReadMessage(bytes.NewBuffer(buf), kr, + func([]Key, bool) ([]byte, error) { + return []byte("insecure"), nil + }, nil) + + if err == nil { + t.Errorf("ReadMessage(): Unexpected nil error") + } +} + +func TestIssue11503(t *testing.T) { + testReadMessageError(t, "8c040402000aa430aa8228b9248b01fc899a91197130303030") +} + +func TestIssue11504(t *testing.T) { + testReadMessageError(t, "9303000130303030303030303030983002303030303030030000000130") +} + +// TestSignatureV3Message tests the verification of V3 signature, generated +// with a modern V4-style key. Some people have their clients set to generate +// V3 signatures, so it's useful to be able to verify them. +func TestSignatureV3Message(t *testing.T) { + sig, err := armor.Decode(strings.NewReader(signedMessageV3)) + if err != nil { + t.Error(err) + return + } + key, err := ReadArmoredKeyRing(strings.NewReader(keyV4forVerifyingSignedMessageV3)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(sig.Body, key, nil, nil) + if err != nil { + t.Error(err) + return + } + + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + // We'll see a sig error here after reading in the UnverifiedBody above, + // if there was one to see. + if err = md.SignatureError; err == nil { + t.Errorf("Expected a signature error") + return + } + + if md.Signature != nil { + t.Errorf("Did not expect a signature V4 back") + return + } +} + +func TestSignatureOldStyleMessage(t *testing.T) { + sig, err := armor.Decode(strings.NewReader(signedMessageOldStyle)) + if err != nil { + t.Error(err) + return + } + key, err := ReadArmoredKeyRing(strings.NewReader(signedMessageOldStyleKey)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(sig.Body, key, nil, nil) + if err != nil { + t.Error(err) + return + } + + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + // We'll see a sig error here after reading in the UnverifiedBody above, + // if there was one to see. + if err = md.SignatureError; err != nil { + t.Error("Expected no signature error, got: ", err) + return + } +} + +func TestReadV6Messages(t *testing.T) { + key, err := ReadArmoredKeyRing(strings.NewReader(v6PrivKey)) + if err != nil { + t.Error(err) + return + } + msgReader, err := armor.Decode(strings.NewReader(v6PrivKeyMsg)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(msgReader.Body, key, nil, nil) + if err != nil { + t.Error(err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if string(contents) != "Hello, world!" { + t.Errorf("decrypted message is wrong: %s", contents) + } + + msgReader, err = armor.Decode(strings.NewReader(v6PrivKeyInlineSignMsg)) + if err != nil { + t.Error(err) + return + } + md, err = ReadMessage(msgReader.Body, key, nil, nil) + if err != nil { + t.Error(err) + return + } + contents, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Error("expected no signature error, got:", md.SignatureError) + } + if string(contents) != "What we need from the grocery store:\n\n- tofu\n- vegetables\n- noodles\n" { + t.Errorf("inline message is wrong: %s", contents) + } +} + +func TestSymmetricDecryptionArgon2(t *testing.T) { + // Appendix IETF OpenPGP crypto refresh draft v08 A.8.1 + passphrase := []byte("password") + file, err := os.Open("../test_data/argon2-sym-message.asc") + if err != nil { + t.Fatal(err) + } + armoredEncryptedMessage, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + // Unarmor string + raw, err := armor.Decode(strings.NewReader(string(armoredEncryptedMessage))) + if err != nil { + t.Error(err) + return + } + // Mock passphrase prompt + promptFunc := func(keys []Key, symmetric bool) ([]byte, error) { + return passphrase, nil + } + // Decrypt message + md, err := ReadMessage(raw.Body, nil, promptFunc, nil) + if err != nil { + t.Error(err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading UnverifiedBody: %s", err) + } + + if string(contents) != "Hello, world!" { + t.Fatal("Did not decrypt Argon message correctly") + } +} + +func TestAsymmestricAeadOcbOpenPGPjsCompressedMessage(t *testing.T) { + // Read key from file + armored, err := os.Open("../test_data/aead-ocb-asym-key.asc") + if err != nil { + t.Fatal(err) + } + el, err := ReadArmoredKeyRing(armored) + if err != nil { + t.Fatal(err) + } + // Read ciphertext from file + ciphertext, err := os.Open("../test_data/aead-ocb-asym-message.asc") + if err != nil { + t.Fatal(err) + } + armoredEncryptedMessage, err := io.ReadAll(ciphertext) + if err != nil { + t.Fatal(err) + } + // Unarmor string + raw, err := armor.Decode(strings.NewReader(string(armoredEncryptedMessage))) + if err != nil { + t.Error(err) + return + } + // Decrypt message + md, err := ReadMessage(raw.Body, el, nil, nil) + if err != nil { + t.Error(err) + return + } + // Read contents + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil && err != io.ErrUnexpectedEOF { + t.Errorf("error reading UnverifiedBody: %s", err) + } + + wantHash := modestProposalSha512 + gotHashRaw := sha512.Sum512(contents) + gotHash := base64.StdEncoding.EncodeToString(gotHashRaw[:]) + + if wantHash != gotHash { + t.Fatal("Did not decrypt OpenPGPjs message correctly") + } +} + +func TestSymmetricAeadEaxOpenPGPJsMessage(t *testing.T) { + key := []byte{79, 41, 206, 112, 224, 133, 140, 223, 27, 61, 227, 57, 114, + 118, 64, 60, 177, 26, 42, 174, 151, 5, 186, 74, 226, 97, 214, 63, 114, 77, + 215, 121} + + file, err := os.Open("../test_data/aead-eax-packet.b64") + if err != nil { + t.Fatal(err) + } + fileBytes, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + // Decode from base 64 + raw, err := base64.StdEncoding.DecodeString(string(fileBytes)) + if err != nil { + t.Fatal(err) + } + r := bytes.NewBuffer(raw) + // Read packet + p, err := packet.Read(r) + if err != nil { + panic(err) + } + + // Decrypt with key + var edp packet.EncryptedDataPacket + if err != nil { + t.Fatal(err) + } + edp = p.(*packet.AEADEncrypted) + rc, err := edp.Decrypt(packet.CipherFunction(0), key) + if err != nil { + panic(err) + } + // Read literal data packet + p, err = packet.Read(rc) + if err != nil { + t.Fatal(err) + } + ld := p.(*packet.LiteralData) + + // Read contents + contents, err := io.ReadAll(ld.Body) + if err != nil && err != io.ErrUnexpectedEOF { + t.Errorf("error reading UnverifiedBody: %s", err) + } + + wantHash := modestProposalSha512 + gotHashRaw := sha512.Sum512(contents) + gotHash := base64.StdEncoding.EncodeToString(gotHashRaw[:]) + + if wantHash != gotHash { + t.Fatal("Did not decrypt OpenPGPjs message correctly") + } +} + +func TestCorruptedMessageInvalidSigHeader(t *testing.T) { + // Decrypt message with corrupted MDC and invalid one-pass-signature header + // Expect parsing errors over unverified decrypted data to be opaque + passphrase := []byte("password") + file, err := os.Open("../test_data/sym-corrupted-message-invalid-sig-header.asc") + if err != nil { + t.Fatal(err) + } + armoredEncryptedMessage, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + // Unarmor string + raw, err := armor.Decode(strings.NewReader(string(armoredEncryptedMessage))) + if err != nil { + t.Error(err) + return + } + // Mock passphrase prompt + promptFunc := func(keys []Key, symmetric bool) ([]byte, error) { + return passphrase, nil + } + const expectedErr string = "openpgp: invalid data: parsing error" + _, observedErr := ReadMessage(raw.Body, nil, promptFunc, nil) + if observedErr.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, observedErr) + } +} + +func TestCorruptedMessageWrongLength(t *testing.T) { + // Decrypt message with wrong length in Literal packet header (length too long) + // Expect parsing errors over unverified decrypted data to be opaque + passphrase := []byte("password") + promptFunc := func(keys []Key, symmetric bool) ([]byte, error) { + return passphrase, nil + } + const expectedErr string = "openpgp: invalid data: parsing error" + + file, err := os.Open("../test_data/sym-corrupted-message-long-length.asc") + if err != nil { + t.Fatal(err) + } + armoredEncryptedMessage, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + raw, err := armor.Decode(strings.NewReader(string(armoredEncryptedMessage))) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(raw.Body, nil, promptFunc, nil) + if err != nil { + t.Error(err) + return + } + _, err = io.ReadAll(md.UnverifiedBody) + if err == nil { + t.Fatal("Parsing error expected") + } + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, err) + } +} + +func TestMessageWithoutMdc(t *testing.T) { + armored, err := os.Open("../test_data/aead-ocb-asym-key.asc") + if err != nil { + t.Fatal(err) + } + defer armored.Close() + + el, err := ReadArmoredKeyRing(armored) + if err != nil { + t.Fatal(err) + } + + armoredMessageWithoutMdc, err := ioutil.ReadFile("../test_data/sym-message-without-mdc.asc") + if err != nil { + t.Fatal(err) + } + + t.Run("fails with InsecureAllowUnauthenticatedMessages disabled", func(t *testing.T) { + messageWithoutMdc, err := armor.Decode(bytes.NewReader(armoredMessageWithoutMdc)) + if err != nil { + t.Fatal(err) + } + + _, err = ReadMessage(messageWithoutMdc.Body, el, nil, nil) + if err == nil { + t.Fatal("reading the message should have failed") + } + }) + + t.Run("succeeds with InsecureAllowUnauthenticatedMessages enabled", func(t *testing.T) { + messageWithoutMdc, err := armor.Decode(bytes.NewReader(armoredMessageWithoutMdc)) + if err != nil { + t.Fatal(err) + } + + md, err := ReadMessage(messageWithoutMdc.Body, el, nil, &packet.Config{ + InsecureAllowUnauthenticatedMessages: true, + }) + if err != nil { + t.Fatal("reading the message should have worked") + } + + b, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatal("reading the message should have worked") + } + + if !bytes.Equal(b, []byte("message without mdc\n")) { + t.Error("unexpected message content") + } + }) +} + +func TestMultiSignedMessage(t *testing.T) { + messageBlock, err := armor.Decode(strings.NewReader(multiSignMessage)) + if err != nil { + t.Fatal(err) + } + + keyring, err := ReadArmoredKeyRing(strings.NewReader(multiSignMessageKey)) + if err != nil { + t.Fatal(err) + } + + md, err := ReadMessage(messageBlock.Body, keyring, nil, nil) + if err != nil { + t.Fatal(err) + } + + if len(md.SignatureCandidates) != 2 { + t.Errorf("expected 2 signature candidates, got: %d", len(md.SignatureCandidates)) + } + + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatal(err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + + if md.Signature == nil { + t.Error("expected a matched verified signatures, got: nil") + } + + if md.SignatureError != nil { + t.Error("expected no signature error, got: ", md.SignatureError) + } + + if md.SignatureCandidates[0].SignatureError == nil { + t.Error("First candidate should fail in verification, got: nil") + } +} + +func TestMalformedMessage(t *testing.T) { + keyring, err := ReadArmoredKeyRing(strings.NewReader(malformedKeyTest)) + if err != nil { + t.Fatal(err) + } + t.Run("Signature + Literal", func(t *testing.T) { + testMalformedMessage(t, keyring, malformedLiteralSignature) + }) + t.Run("Two literals, 1st compressed 4 times", func(t *testing.T) { + testMalformedMessage(t, keyring, malformedTwoLiteralsCompressed) + }) + t.Run("PKESK + Literal", func(t *testing.T) { + testMalformedMessage(t, keyring, malformedPKESKLiteral) + }) + t.Run("OPS + Literal", func(t *testing.T) { + testMalformedMessage(t, keyring, malformedOPSLiteral) + }) +} + +func testMalformedMessage(t *testing.T, keyring EntityList, message string) { + raw, err := armor.Decode(strings.NewReader(message)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(raw.Body, keyring, nil, nil) + if err != nil { + return + } + _, err = io.ReadAll(md.UnverifiedBody) + if err == nil { + t.Error("Expected malformed message error") + return + } +} diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go new file mode 100644 index 000000000..2f0efc228 --- /dev/null +++ b/openpgp/v2/read_write_test_data.go @@ -0,0 +1,742 @@ +package v2 + +const testKey1KeyId uint64 = 0xA34D7E18C20C31BB +const testKey3KeyId uint64 = 0x338934250CCC0360 +const testKeyP256KeyId uint64 = 0xd44a2c495918513e + +const signedInput = "Signed message\nline 2\nline 3\n" +const signedTextInput = "Signed message\r\nline 2\r\nline 3\r\n" + +const recipientUnspecifiedHex = "848c0300000000000000000103ff62d4d578d03cf40c3da998dfe216c074fa6ddec5e31c197c9666ba292830d91d18716a80f699f9d897389a90e6d62d0238f5f07a5248073c0f24920e4bc4a30c2d17ee4e0cae7c3d4aaa4e8dced50e3010a80ee692175fa0385f62ecca4b56ee6e9980aa3ec51b61b077096ac9e800edaf161268593eedb6cc7027ff5cb32745d250010d407a6221ae22ef18469b444f2822478c4d190b24d36371a95cb40087cdd42d9399c3d06a53c0673349bfb607927f20d1e122bde1e2bf3aa6cae6edf489629bcaa0689539ae3b718914d88ededc3b" + +const detachedSignatureHex = "889c04000102000605024d449cd1000a0910a34d7e18c20c31bb167603ff57718d09f28a519fdc7b5a68b6a3336da04df85e38c5cd5d5bd2092fa4629848a33d85b1729402a2aab39c3ac19f9d573f773cc62c264dc924c067a79dfd8a863ae06c7c8686120760749f5fd9b1e03a64d20a7df3446ddc8f0aeadeaeba7cbaee5c1e366d65b6a0c6cc749bcb912d2f15013f812795c2e29eb7f7b77f39ce77" + +const detachedSignatureTextHex = "889c04010102000605024d449d21000a0910a34d7e18c20c31bbc8c60400a24fbef7342603a41cb1165767bd18985d015fb72fe05db42db36cfb2f1d455967f1e491194fbf6cf88146222b23bf6ffbd50d17598d976a0417d3192ff9cc0034fd00f287b02e90418bbefe609484b09231e4e7a5f3562e199bf39909ab5276c4d37382fe088f6b5c3426fc1052865da8b3ab158672d58b6264b10823dc4b39" + +const detachedSignatureDSAHex = "884604001102000605024d6c4eac000a0910338934250ccc0360f18d00a087d743d6405ed7b87755476629600b8b694a39e900a0abff8126f46faf1547c1743c37b21b4ea15b8f83" + +const detachedSignatureP256Hex = "885e0400130a0006050256e5bb00000a0910d44a2c495918513edef001009841a4f792beb0befccb35c8838a6a87d9b936beaa86db6745ddc7b045eee0cf00fd1ac1f78306b17e965935dd3f8bae4587a76587e4af231efe19cc4011a8434817" + +// The plaintext is https://www.gutenberg.org/cache/epub/1080/pg1080.txt +const modestProposalSha512 = "lbbrB1+WP3T9AaC9OQqBdOcCjgeEQadlulXsNPgVx0tyqPzDHwUugZ2gE7V0ESKAw6kAVfgkcuvfgxAAGaeHtw==" + +const testKeys1And2Hex = "988d044d3c5c10010400b1d13382944bd5aba23a4312968b5095d14f947f600eb478e14a6fcb16b0e0cac764884909c020bc495cfcc39a935387c661507bdb236a0612fb582cac3af9b29cc2c8c70090616c41b662f4da4c1201e195472eb7f4ae1ccbcbf9940fe21d985e379a5563dde5b9a23d35f1cfaa5790da3b79db26f23695107bfaca8e7b5bcd0011010001b41054657374204b6579203120285253412988b804130102002205024d3c5c10021b03060b090807030206150802090a0b0416020301021e01021780000a0910a34d7e18c20c31bbb5b304009cc45fe610b641a2c146331be94dade0a396e73ca725e1b25c21708d9cab46ecca5ccebc23055879df8f99eea39b377962a400f2ebdc36a7c99c333d74aeba346315137c3ff9d0a09b0273299090343048afb8107cf94cbd1400e3026f0ccac7ecebbc4d78588eb3e478fe2754d3ca664bcf3eac96ca4a6b0c8d7df5102f60f6b0020003b88d044d3c5c10010400b201df61d67487301f11879d514f4248ade90c8f68c7af1284c161098de4c28c2850f1ec7b8e30f959793e571542ffc6532189409cb51c3d30dad78c4ad5165eda18b20d9826d8707d0f742e2ab492103a85bbd9ddf4f5720f6de7064feb0d39ee002219765bb07bcfb8b877f47abe270ddeda4f676108cecb6b9bb2ad484a4f0011010001889f04180102000905024d3c5c10021b0c000a0910a34d7e18c20c31bb1a03040085c8d62e16d05dc4e9dad64953c8a2eed8b6c12f92b1575eeaa6dcf7be9473dd5b24b37b6dffbb4e7c99ed1bd3cb11634be19b3e6e207bed7505c7ca111ccf47cb323bf1f8851eb6360e8034cbff8dd149993c959de89f8f77f38e7e98b8e3076323aa719328e2b408db5ec0d03936efd57422ba04f925cdc7b4c1af7590e40ab0020003988d044d3c5c33010400b488c3e5f83f4d561f317817538d9d0397981e9aef1321ca68ebfae1cf8b7d388e19f4b5a24a82e2fbbf1c6c26557a6c5845307a03d815756f564ac7325b02bc83e87d5480a8fae848f07cb891f2d51ce7df83dcafdc12324517c86d472cc0ee10d47a68fd1d9ae49a6c19bbd36d82af597a0d88cc9c49de9df4e696fc1f0b5d0011010001b42754657374204b6579203220285253412c20656e637279707465642070726976617465206b65792988b804130102002205024d3c5c33021b03060b090807030206150802090a0b0416020301021e01021780000a0910d4984f961e35246b98940400908a73b6a6169f700434f076c6c79015a49bee37130eaf23aaa3cfa9ce60bfe4acaa7bc95f1146ada5867e0079babb38804891f4f0b8ebca57a86b249dee786161a755b7a342e68ccf3f78ed6440a93a6626beb9a37aa66afcd4f888790cb4bb46d94a4ae3eb3d7d3e6b00f6bfec940303e89ec5b32a1eaaacce66497d539328b0020003b88d044d3c5c33010400a4e913f9442abcc7f1804ccab27d2f787ffa592077ca935a8bb23165bd8d57576acac647cc596b2c3f814518cc8c82953c7a4478f32e0cf645630a5ba38d9618ef2bc3add69d459ae3dece5cab778938d988239f8c5ae437807075e06c828019959c644ff05ef6a5a1dab72227c98e3a040b0cf219026640698d7a13d8538a570011010001889f04180102000905024d3c5c33021b0c000a0910d4984f961e35246b26c703ff7ee29ef53bc1ae1ead533c408fa136db508434e233d6e62be621e031e5940bbd4c08142aed0f82217e7c3e1ec8de574bc06ccf3c36633be41ad78a9eacd209f861cae7b064100758545cc9dd83db71806dc1cfd5fb9ae5c7474bba0c19c44034ae61bae5eca379383339dece94ff56ff7aa44a582f3e5c38f45763af577c0934b0020003" + +const testKeys1And2PrivateHex = "9501d8044d3c5c10010400b1d13382944bd5aba23a4312968b5095d14f947f600eb478e14a6fcb16b0e0cac764884909c020bc495cfcc39a935387c661507bdb236a0612fb582cac3af9b29cc2c8c70090616c41b662f4da4c1201e195472eb7f4ae1ccbcbf9940fe21d985e379a5563dde5b9a23d35f1cfaa5790da3b79db26f23695107bfaca8e7b5bcd00110100010003ff4d91393b9a8e3430b14d6209df42f98dc927425b881f1209f319220841273a802a97c7bdb8b3a7740b3ab5866c4d1d308ad0d3a79bd1e883aacf1ac92dfe720285d10d08752a7efe3c609b1d00f17f2805b217be53999a7da7e493bfc3e9618fd17018991b8128aea70a05dbce30e4fbe626aa45775fa255dd9177aabf4df7cf0200c1ded12566e4bc2bb590455e5becfb2e2c9796482270a943343a7835de41080582c2be3caf5981aa838140e97afa40ad652a0b544f83eb1833b0957dce26e47b0200eacd6046741e9ce2ec5beb6fb5e6335457844fb09477f83b050a96be7da043e17f3a9523567ed40e7a521f818813a8b8a72209f1442844843ccc7eb9805442570200bdafe0438d97ac36e773c7162028d65844c4d463e2420aa2228c6e50dc2743c3d6c72d0d782a5173fe7be2169c8a9f4ef8a7cf3e37165e8c61b89c346cdc6c1799d2b41054657374204b6579203120285253412988b804130102002205024d3c5c10021b03060b090807030206150802090a0b0416020301021e01021780000a0910a34d7e18c20c31bbb5b304009cc45fe610b641a2c146331be94dade0a396e73ca725e1b25c21708d9cab46ecca5ccebc23055879df8f99eea39b377962a400f2ebdc36a7c99c333d74aeba346315137c3ff9d0a09b0273299090343048afb8107cf94cbd1400e3026f0ccac7ecebbc4d78588eb3e478fe2754d3ca664bcf3eac96ca4a6b0c8d7df5102f60f6b00200009d01d8044d3c5c10010400b201df61d67487301f11879d514f4248ade90c8f68c7af1284c161098de4c28c2850f1ec7b8e30f959793e571542ffc6532189409cb51c3d30dad78c4ad5165eda18b20d9826d8707d0f742e2ab492103a85bbd9ddf4f5720f6de7064feb0d39ee002219765bb07bcfb8b877f47abe270ddeda4f676108cecb6b9bb2ad484a4f00110100010003fd17a7490c22a79c59281fb7b20f5e6553ec0c1637ae382e8adaea295f50241037f8997cf42c1ce26417e015091451b15424b2c59eb8d4161b0975630408e394d3b00f88d4b4e18e2cc85e8251d4753a27c639c83f5ad4a571c4f19d7cd460b9b73c25ade730c99df09637bd173d8e3e981ac64432078263bb6dc30d3e974150dd0200d0ee05be3d4604d2146fb0457f31ba17c057560785aa804e8ca5530a7cd81d3440d0f4ba6851efcfd3954b7e68908fc0ba47f7ac37bf559c6c168b70d3a7c8cd0200da1c677c4bce06a068070f2b3733b0a714e88d62aa3f9a26c6f5216d48d5c2b5624144f3807c0df30be66b3268eeeca4df1fbded58faf49fc95dc3c35f134f8b01fd1396b6c0fc1b6c4f0eb8f5e44b8eace1e6073e20d0b8bc5385f86f1cf3f050f66af789f3ef1fc107b7f4421e19e0349c730c68f0a226981f4e889054fdb4dc149e8e889f04180102000905024d3c5c10021b0c000a0910a34d7e18c20c31bb1a03040085c8d62e16d05dc4e9dad64953c8a2eed8b6c12f92b1575eeaa6dcf7be9473dd5b24b37b6dffbb4e7c99ed1bd3cb11634be19b3e6e207bed7505c7ca111ccf47cb323bf1f8851eb6360e8034cbff8dd149993c959de89f8f77f38e7e98b8e3076323aa719328e2b408db5ec0d03936efd57422ba04f925cdc7b4c1af7590e40ab00200009501fe044d3c5c33010400b488c3e5f83f4d561f317817538d9d0397981e9aef1321ca68ebfae1cf8b7d388e19f4b5a24a82e2fbbf1c6c26557a6c5845307a03d815756f564ac7325b02bc83e87d5480a8fae848f07cb891f2d51ce7df83dcafdc12324517c86d472cc0ee10d47a68fd1d9ae49a6c19bbd36d82af597a0d88cc9c49de9df4e696fc1f0b5d0011010001fe030302e9030f3c783e14856063f16938530e148bc57a7aa3f3e4f90df9dceccdc779bc0835e1ad3d006e4a8d7b36d08b8e0de5a0d947254ecfbd22037e6572b426bcfdc517796b224b0036ff90bc574b5509bede85512f2eefb520fb4b02aa523ba739bff424a6fe81c5041f253f8d757e69a503d3563a104d0d49e9e890b9d0c26f96b55b743883b472caa7050c4acfd4a21f875bdf1258d88bd61224d303dc9df77f743137d51e6d5246b88c406780528fd9a3e15bab5452e5b93970d9dcc79f48b38651b9f15bfbcf6da452837e9cc70683d1bdca94507870f743e4ad902005812488dd342f836e72869afd00ce1850eea4cfa53ce10e3608e13d3c149394ee3cbd0e23d018fcbcb6e2ec5a1a22972d1d462ca05355d0d290dd2751e550d5efb38c6c89686344df64852bf4ff86638708f644e8ec6bd4af9b50d8541cb91891a431326ab2e332faa7ae86cfb6e0540aa63160c1e5cdd5a4add518b303fff0a20117c6bc77f7cfbaf36b04c865c6c2b42754657374204b6579203220285253412c20656e637279707465642070726976617465206b65792988b804130102002205024d3c5c33021b03060b090807030206150802090a0b0416020301021e01021780000a0910d4984f961e35246b98940400908a73b6a6169f700434f076c6c79015a49bee37130eaf23aaa3cfa9ce60bfe4acaa7bc95f1146ada5867e0079babb38804891f4f0b8ebca57a86b249dee786161a755b7a342e68ccf3f78ed6440a93a6626beb9a37aa66afcd4f888790cb4bb46d94a4ae3eb3d7d3e6b00f6bfec940303e89ec5b32a1eaaacce66497d539328b00200009d01fe044d3c5c33010400a4e913f9442abcc7f1804ccab27d2f787ffa592077ca935a8bb23165bd8d57576acac647cc596b2c3f814518cc8c82953c7a4478f32e0cf645630a5ba38d9618ef2bc3add69d459ae3dece5cab778938d988239f8c5ae437807075e06c828019959c644ff05ef6a5a1dab72227c98e3a040b0cf219026640698d7a13d8538a570011010001fe030302e9030f3c783e148560f936097339ae381d63116efcf802ff8b1c9360767db5219cc987375702a4123fd8657d3e22700f23f95020d1b261eda5257e9a72f9a918e8ef22dd5b3323ae03bbc1923dd224db988cadc16acc04b120a9f8b7e84da9716c53e0334d7b66586ddb9014df604b41be1e960dcfcbc96f4ed150a1a0dd070b9eb14276b9b6be413a769a75b519a53d3ecc0c220e85cd91ca354d57e7344517e64b43b6e29823cbd87eae26e2b2e78e6dedfbb76e3e9f77bcb844f9a8932eb3db2c3f9e44316e6f5d60e9e2a56e46b72abe6b06dc9a31cc63f10023d1f5e12d2a3ee93b675c96f504af0001220991c88db759e231b3320dcedf814dcf723fd9857e3d72d66a0f2af26950b915abdf56c1596f46a325bf17ad4810d3535fb02a259b247ac3dbd4cc3ecf9c51b6c07cebb009c1506fba0a89321ec8683e3fd009a6e551d50243e2d5092fefb3321083a4bad91320dc624bd6b5dddf93553e3d53924c05bfebec1fb4bd47e89a1a889f04180102000905024d3c5c33021b0c000a0910d4984f961e35246b26c703ff7ee29ef53bc1ae1ead533c408fa136db508434e233d6e62be621e031e5940bbd4c08142aed0f82217e7c3e1ec8de574bc06ccf3c36633be41ad78a9eacd209f861cae7b064100758545cc9dd83db71806dc1cfd5fb9ae5c7474bba0c19c44034ae61bae5eca379383339dece94ff56ff7aa44a582f3e5c38f45763af577c0934b0020000" + +const dsaElGamalTestKeysHex = "9501e1044dfcb16a110400aa3e5c1a1f43dd28c2ffae8abf5cfce555ee874134d8ba0a0f7b868ce2214beddc74e5e1e21ded354a95d18acdaf69e5e342371a71fbb9093162e0c5f3427de413a7f2c157d83f5cd2f9d791256dc4f6f0e13f13c3302af27f2384075ab3021dff7a050e14854bbde0a1094174855fc02f0bae8e00a340d94a1f22b32e48485700a0cec672ac21258fb95f61de2ce1af74b2c4fa3e6703ff698edc9be22c02ae4d916e4fa223f819d46582c0516235848a77b577ea49018dcd5e9e15cff9dbb4663a1ae6dd7580fa40946d40c05f72814b0f88481207e6c0832c3bded4853ebba0a7e3bd8e8c66df33d5a537cd4acf946d1080e7a3dcea679cb2b11a72a33a2b6a9dc85f466ad2ddf4c3db6283fa645343286971e3dd700703fc0c4e290d45767f370831a90187e74e9972aae5bff488eeff7d620af0362bfb95c1a6c3413ab5d15a2e4139e5d07a54d72583914661ed6a87cce810be28a0aa8879a2dd39e52fb6fe800f4f181ac7e328f740cde3d09a05cecf9483e4cca4253e60d4429ffd679d9996a520012aad119878c941e3cf151459873bdfc2a9563472fe0303027a728f9feb3b864260a1babe83925ce794710cfd642ee4ae0e5b9d74cee49e9c67b6cd0ea5dfbb582132195a121356a1513e1bca73e5b80c58c7ccb4164453412f456c47616d616c2054657374204b65792031886204131102002205024dfcb16a021b03060b090807030206150802090a0b0416020301021e01021780000a091033af447ccd759b09fadd00a0b8fd6f5a790bad7e9f2dbb7632046dc4493588db009c087c6a9ba9f7f49fab221587a74788c00db4889ab00200009d0157044dfcb16a1004008dec3f9291205255ccff8c532318133a6840739dd68b03ba942676f9038612071447bf07d00d559c5c0875724ea16a4c774f80d8338b55fca691a0522e530e604215b467bbc9ccfd483a1da99d7bc2648b4318fdbd27766fc8bfad3fddb37c62b8ae7ccfe9577e9b8d1e77c1d417ed2c2ef02d52f4da11600d85d3229607943700030503ff506c94c87c8cab778e963b76cf63770f0a79bf48fb49d3b4e52234620fc9f7657f9f8d56c96a2b7c7826ae6b57ebb2221a3fe154b03b6637cea7e6d98e3e45d87cf8dc432f723d3d71f89c5192ac8d7290684d2c25ce55846a80c9a7823f6acd9bb29fa6cd71f20bc90eccfca20451d0c976e460e672b000df49466408d527affe0303027a728f9feb3b864260abd761730327bca2aaa4ea0525c175e92bf240682a0e83b226f97ecb2e935b62c9a133858ce31b271fa8eb41f6a1b3cd72a63025ce1a75ee4180dcc284884904181102000905024dfcb16a021b0c000a091033af447ccd759b09dd0b009e3c3e7296092c81bee5a19929462caaf2fff3ae26009e218c437a2340e7ea628149af1ec98ec091a43992b00200009501e1044dfcb1be1104009f61faa61aa43df75d128cbe53de528c4aec49ce9360c992e70c77072ad5623de0a3a6212771b66b39a30dad6781799e92608316900518ec01184a85d872365b7d2ba4bacfb5882ea3c2473d3750dc6178cc1cf82147fb58caa28b28e9f12f6d1efcb0534abed644156c91cca4ab78834268495160b2400bc422beb37d237c2300a0cac94911b6d493bda1e1fbc6feeca7cb7421d34b03fe22cec6ccb39675bb7b94a335c2b7be888fd3906a1125f33301d8aa6ec6ee6878f46f73961c8d57a3e9544d8ef2a2cbfd4d52da665b1266928cfe4cb347a58c412815f3b2d2369dec04b41ac9a71cc9547426d5ab941cccf3b18575637ccfb42df1a802df3cfe0a999f9e7109331170e3a221991bf868543960f8c816c28097e503fe319db10fb98049f3a57d7c80c420da66d56f3644371631fad3f0ff4040a19a4fedc2d07727a1b27576f75a4d28c47d8246f27071e12d7a8de62aad216ddbae6aa02efd6b8a3e2818cda48526549791ab277e447b3a36c57cefe9b592f5eab73959743fcc8e83cbefec03a329b55018b53eec196765ae40ef9e20521a603c551efe0303020950d53a146bf9c66034d00c23130cce95576a2ff78016ca471276e8227fb30b1ffbd92e61804fb0c3eff9e30b1a826ee8f3e4730b4d86273ca977b4164453412f456c47616d616c2054657374204b65792032886204131102002205024dfcb1be021b03060b090807030206150802090a0b0416020301021e01021780000a0910a86bf526325b21b22bd9009e34511620415c974750a20df5cb56b182f3b48e6600a0a9466cb1a1305a84953445f77d461593f1d42bc1b00200009d0157044dfcb1be1004009565a951da1ee87119d600c077198f1c1bceb0f7aa54552489298e41ff788fa8f0d43a69871f0f6f77ebdfb14a4260cf9fbeb65d5844b4272a1904dd95136d06c3da745dc46327dd44a0f16f60135914368c8039a34033862261806bb2c5ce1152e2840254697872c85441ccb7321431d75a747a4bfb1d2c66362b51ce76311700030503fc0ea76601c196768070b7365a200e6ddb09307f262d5f39eec467b5f5784e22abdf1aa49226f59ab37cb49969d8f5230ea65caf56015abda62604544ed526c5c522bf92bed178a078789f6c807b6d34885688024a5bed9e9f8c58d11d4b82487b44c5f470c5606806a0443b79cadb45e0f897a561a53f724e5349b9267c75ca17fe0303020950d53a146bf9c660bc5f4ce8f072465e2d2466434320c1e712272fafc20e342fe7608101580fa1a1a367e60486a7cd1246b7ef5586cf5e10b32762b710a30144f12dd17dd4884904181102000905024dfcb1be021b0c000a0910a86bf526325b21b2904c00a0b2b66b4b39ccffda1d10f3ea8d58f827e30a8b8e009f4255b2d8112a184e40cde43a34e8655ca7809370b0020000" + +const ed25519wX25519Key = "c54b0663877fe31b00000020f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3001972817b12be707e8d5f586ce61361201d344eb266a2c82fde6835762b65b0b7c2b1061f1b0a00000042058263877fe3030b090705150a0e080c021600029b03021e09222106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc905270902070200000000ad2820103e2d7d227ec0e6d7ce4471db36bfc97083253690271498a7ef0576c07faae14585b3b903b0127ec4fda2f023045a2ec76bcb4f9571a9651e14aee1137a1d668442c88f951e33c4ffd33fb9a17d511eed758fc6d9cc50cb5fd793b2039d5804c74b0663877fe319000000208693248367f9e5015db922f8f48095dda784987f2d5985b12fbad16caf5e4435004d600a4f794d44775c57a26e0feefed558e9afffd6ad0d582d57fb2ba2dcedb8c29b06181b0a0000002c050263877fe322a106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc9021b0c00000000defa20a6e9186d9d5935fc8fe56314cdb527486a5a5120f9b762a235a729f039010a56b89c658568341fbef3b894e9834ad9bc72afae2f4c9c47a43855e65f1cb0a3f77bbc5f61085c1f8249fe4e7ca59af5f0bcee9398e0fa8d76e522e1d8ab42bb0d" + +const signedMessageHex = "a3019bc0cbccc0c4b8d8b74ee2108fe16ec6d3ca490cbe362d3f8333d3f352531472538b8b13d353b97232f352158c20943157c71c16064626063656269052062e4e01987e9b6fccff4b7df3a34c534b23e679cbec3bc0f8f6e64dfb4b55fe3f8efa9ce110ddb5cd79faf1d753c51aecfa669f7e7aa043436596cccc3359cb7dd6bbe9ecaa69e5989d9e57209571edc0b2fa7f57b9b79a64ee6e99ce1371395fee92fec2796f7b15a77c386ff668ee27f6d38f0baa6c438b561657377bf6acff3c5947befd7bf4c196252f1d6e5c524d0300" + +const signedTextMessageHex = "a3019bc0cbccc8c4b8d8b74ee2108fe16ec6d36a250cbece0c178233d3f352531472538b8b13d35379b97232f352158ca0b4312f57c71c1646462606365626906a062e4e019811591798ff99bf8afee860b0d8a8c2a85c3387e3bcf0bb3b17987f2bbcfab2aa526d930cbfd3d98757184df3995c9f3e7790e36e3e9779f06089d4c64e9e47dd6202cb6e9bc73c5d11bb59fbaf89d22d8dc7cf199ddf17af96e77c5f65f9bbed56f427bd8db7af37f6c9984bf9385efaf5f184f986fb3e6adb0ecfe35bbf92d16a7aa2a344fb0bc52fb7624f0200" + +const signedEncryptedMessageHex = "c18c032a67d68660df41c70103ff5a84c9a72f80e74ef0384c2d6a9ebfe2b09e06a8f298394f6d2abf174e40934ab0ec01fb2d0ddf21211c6fe13eb238563663b017a6b44edca552eb4736c4b7dc6ed907dd9e12a21b51b64b46f902f76fb7aaf805c1db8070574d8d0431a23e324a750f77fb72340a17a42300ee4ca8207301e95a731da229a63ab9c6b44541fbd2c11d016d810b3b3b2b38f15b5b40f0a4910332829c2062f1f7cc61f5b03677d73c54cafa1004ced41f315d46444946faae571d6f426e6dbd45d9780eb466df042005298adabf7ce0ef766dfeb94cd449c7ed0046c880339599c4711af073ce649b1e237c40b50a5536283e03bdbb7afad78bd08707715c67fb43295f905b4c479178809d429a8e167a9a8c6dfd8ab20b4edebdc38d6dec879a3202e1b752690d9bb5b0c07c5a227c79cc200e713a99251a4219d62ad5556900cf69bd384b6c8e726c7be267471d0d23af956da165af4af757246c2ebcc302b39e8ef2fccb4971b234fcda22d759ddb20e27269ee7f7fe67898a9de721bfa02ab0becaa046d00ea16cb1afc4e2eab40d0ac17121c565686e5cbd0cbdfbd9d6db5c70278b9c9db5a83176d04f61fbfbc4471d721340ede2746e5c312ded4f26787985af92b64fae3f253dbdde97f6a5e1996fd4d865599e32ff76325d3e9abe93184c02988ee89a4504356a4ef3b9b7a57cbb9637ca90af34a7676b9ef559325c3cca4e29d69fec1887f5440bb101361d744ad292a8547f22b4f22b419a42aa836169b89190f46d9560824cb2ac6e8771de8223216a5e647e132ab9eebcba89569ab339cb1c3d70fe806b31f4f4c600b4103b8d7583ebff16e43dcda551e6530f975122eb8b29" + +const verifiedSignatureEncryptedMessageHex = "c2b304000108000605026048f6d600210910a34d7e18c20c31bb1621045fb74b1d03b1e3cb31bc2f8aa34d7e18c20c31bb9a3b0400a32ddac1af259c1b0abab0041327ea04970944401978fb647dd1cf9aba4f164e43f0d8a9389501886474bdd4a6e77f6aea945c07dfbf87743835b44cc2c39a1f9aeecfa83135abc92e18e50396f2e6a06c44e0188b0081effbfb4160d28f118d4ff73dd199a102e47cffd8c7ff2bacd83ae72b5820c021a486766dd587b5da61" + +const unverifiedSignatureEncryptedMessageHex = "c2b304000108000605026048f6d600210910d4984f961e35246b162104f7745a3c5e5fce108c1f128bd4984f961e35246b9a3b04009f3f90cf495f4e1cf759d1b596eea8fd31f3ac9ffb71e5bd5412e1834410dc4ba7f5a73481f48feabadd676c81e7329f894dedf87b3a74e01465515744baee12c9bcd7f16722f831f09459c12fd822f492da4236a7728b4e3f4e68f1eee4e68d21b9e71dfd7832cb42725a69b9c6468d47b01d9d51f57f64b47f291983a3f635" + +const signedEncryptedMessage2Hex = "85010e03cf6a7abcd43e36731003fb057f5495b79db367e277cdbe4ab90d924ddee0c0381494112ff8c1238fb0184af35d1731573b01bc4c55ecacd2aafbe2003d36310487d1ecc9ac994f3fada7f9f7f5c3a64248ab7782906c82c6ff1303b69a84d9a9529c31ecafbcdb9ba87e05439897d87e8a2a3dec55e14df19bba7f7bd316291c002ae2efd24f83f9e3441203fc081c0c23dc3092a454ca8a082b27f631abf73aca341686982e8fbda7e0e7d863941d68f3de4a755c2964407f4b5e0477b3196b8c93d551dd23c8beef7d0f03fbb1b6066f78907faf4bf1677d8fcec72651124080e0b7feae6b476e72ab207d38d90b958759fdedfc3c6c35717c9dbfc979b3cfbbff0a76d24a5e57056bb88acbd2a901ef64bc6e4db02adc05b6250ff378de81dca18c1910ab257dff1b9771b85bb9bbe0a69f5989e6d1710a35e6dfcceb7d8fb5ccea8db3932b3d9ff3fe0d327597c68b3622aec8e3716c83a6c93f497543b459b58ba504ed6bcaa747d37d2ca746fe49ae0a6ce4a8b694234e941b5159ff8bd34b9023da2814076163b86f40eed7c9472f81b551452d5ab87004a373c0172ec87ea6ce42ccfa7dbdad66b745496c4873d8019e8c28d6b3" + +const signatureEncryptedMessage2Hex = "c24604001102000605024dfd0166000a091033af447ccd759b09bae600a096ec5e63ecf0a403085e10f75cc3bab327663282009f51fad9df457ed8d2b70d8a73c76e0443eac0f377" + +const symmetricallyEncryptedCompressedHex = "c32e040903085a357c1a7b5614ed00cc0d1d92f428162058b3f558a0fb0980d221ebac6c97d5eda4e0fe32f6e706e94dd263012d6ca1ef8c4bbd324098225e603a10c85ebf09cbf7b5aeeb5ce46381a52edc51038b76a8454483be74e6dcd1e50d5689a8ae7eceaeefed98a0023d49b22eb1f65c2aa1ef1783bb5e1995713b0457102ec3c3075fe871267ffa4b686ad5d52000d857" + +const dsaTestKeyHex = "9901a2044d6c49de110400cb5ce438cf9250907ac2ba5bf6547931270b89f7c4b53d9d09f4d0213a5ef2ec1f26806d3d259960f872a4a102ef1581ea3f6d6882d15134f21ef6a84de933cc34c47cc9106efe3bd84c6aec12e78523661e29bc1a61f0aab17fa58a627fd5fd33f5149153fbe8cd70edf3d963bc287ef875270ff14b5bfdd1bca4483793923b00a0fe46d76cb6e4cbdc568435cd5480af3266d610d303fe33ae8273f30a96d4d34f42fa28ce1112d425b2e3bf7ea553d526e2db6b9255e9dc7419045ce817214d1a0056dbc8d5289956a4b1b69f20f1105124096e6a438f41f2e2495923b0f34b70642607d45559595c7fe94d7fa85fc41bf7d68c1fd509ebeaa5f315f6059a446b9369c277597e4f474a9591535354c7e7f4fd98a08aa60400b130c24ff20bdfbf683313f5daebf1c9b34b3bdadfc77f2ddd72ee1fb17e56c473664bc21d66467655dd74b9005e3a2bacce446f1920cd7017231ae447b67036c9b431b8179deacd5120262d894c26bc015bffe3d827ba7087ad9b700d2ca1f6d16cc1786581e5dd065f293c31209300f9b0afcc3f7c08dd26d0a22d87580b4db41054657374204b65792033202844534129886204131102002205024d6c49de021b03060b090807030206150802090a0b0416020301021e01021780000a0910338934250ccc03607e0400a0bdb9193e8a6b96fc2dfc108ae848914b504481f100a09c4dc148cb693293a67af24dd40d2b13a9e36794" + +const dsaTestKeyPrivateHex = "9501bb044d6c49de110400cb5ce438cf9250907ac2ba5bf6547931270b89f7c4b53d9d09f4d0213a5ef2ec1f26806d3d259960f872a4a102ef1581ea3f6d6882d15134f21ef6a84de933cc34c47cc9106efe3bd84c6aec12e78523661e29bc1a61f0aab17fa58a627fd5fd33f5149153fbe8cd70edf3d963bc287ef875270ff14b5bfdd1bca4483793923b00a0fe46d76cb6e4cbdc568435cd5480af3266d610d303fe33ae8273f30a96d4d34f42fa28ce1112d425b2e3bf7ea553d526e2db6b9255e9dc7419045ce817214d1a0056dbc8d5289956a4b1b69f20f1105124096e6a438f41f2e2495923b0f34b70642607d45559595c7fe94d7fa85fc41bf7d68c1fd509ebeaa5f315f6059a446b9369c277597e4f474a9591535354c7e7f4fd98a08aa60400b130c24ff20bdfbf683313f5daebf1c9b34b3bdadfc77f2ddd72ee1fb17e56c473664bc21d66467655dd74b9005e3a2bacce446f1920cd7017231ae447b67036c9b431b8179deacd5120262d894c26bc015bffe3d827ba7087ad9b700d2ca1f6d16cc1786581e5dd065f293c31209300f9b0afcc3f7c08dd26d0a22d87580b4d00009f592e0619d823953577d4503061706843317e4fee083db41054657374204b65792033202844534129886204131102002205024d6c49de021b03060b090807030206150802090a0b0416020301021e01021780000a0910338934250ccc03607e0400a0bdb9193e8a6b96fc2dfc108ae848914b504481f100a09c4dc148cb693293a67af24dd40d2b13a9e36794" + +const p256TestKeyHex = "98520456e5b83813082a8648ce3d030107020304a2072cd6d21321266c758cc5b83fab0510f751cb8d91897cddb7047d8d6f185546e2107111b0a95cb8ef063c33245502af7a65f004d5919d93ee74eb71a66253b424502d3235362054657374204b6579203c696e76616c6964406578616d706c652e636f6d3e8879041313080021050256e5b838021b03050b09080702061508090a0b020416020301021e01021780000a0910d44a2c495918513e54e50100dfa64f97d9b47766fc1943c6314ba3f2b2a103d71ad286dc5b1efb96a345b0c80100dbc8150b54241f559da6ef4baacea6d31902b4f4b1bdc09b34bf0502334b7754b8560456e5b83812082a8648ce3d030107020304bfe3cea9cee13486f8d518aa487fecab451f25467d2bf08e58f63e5fa525d5482133e6a79299c274b068ef0be448152ad65cf11cf764348588ca4f6a0bcf22b6030108078861041813080009050256e5b838021b0c000a0910d44a2c495918513e4a4800ff49d589fa64024ad30be363a032e3a0e0e6f5db56ba4c73db850518bf0121b8f20100fd78e065f4c70ea5be9df319ea67e493b936fc78da834a71828043d3154af56e" + +const p256TestKeyPrivateHex = "94a50456e5b83813082a8648ce3d030107020304a2072cd6d21321266c758cc5b83fab0510f751cb8d91897cddb7047d8d6f185546e2107111b0a95cb8ef063c33245502af7a65f004d5919d93ee74eb71a66253fe070302f0c2bfb0b6c30f87ee1599472b8636477eab23ced13b271886a4b50ed34c9d8436af5af5b8f88921f0efba6ef8c37c459bbb88bc1c6a13bbd25c4ce9b1e97679569ee77645d469bf4b43de637f5561b424502d3235362054657374204b6579203c696e76616c6964406578616d706c652e636f6d3e8879041313080021050256e5b838021b03050b09080702061508090a0b020416020301021e01021780000a0910d44a2c495918513e54e50100dfa64f97d9b47766fc1943c6314ba3f2b2a103d71ad286dc5b1efb96a345b0c80100dbc8150b54241f559da6ef4baacea6d31902b4f4b1bdc09b34bf0502334b77549ca90456e5b83812082a8648ce3d030107020304bfe3cea9cee13486f8d518aa487fecab451f25467d2bf08e58f63e5fa525d5482133e6a79299c274b068ef0be448152ad65cf11cf764348588ca4f6a0bcf22b603010807fe0703027510012471a603cfee2968dce19f732721ddf03e966fd133b4e3c7a685b788705cbc46fb026dc94724b830c9edbaecd2fb2c662f23169516cacd1fe423f0475c364ecc10abcabcfd4bbbda1a36a1bd8861041813080009050256e5b838021b0c000a0910d44a2c495918513e4a4800ff49d589fa64024ad30be363a032e3a0e0e6f5db56ba4c73db850518bf0121b8f20100fd78e065f4c70ea5be9df319ea67e493b936fc78da834a71828043d3154af56e" + +const armoredPrivateKeyBlock = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +lQHYBE2rFNoBBADFwqWQIW/DSqcB4yCQqnAFTJ27qS5AnB46ccAdw3u4Greeu3Bp +idpoHdjULy7zSKlwR1EA873dO/k/e11Ml3dlAFUinWeejWaK2ugFP6JjiieSsrKn +vWNicdCS4HTWn0X4sjl0ZiAygw6GNhqEQ3cpLeL0g8E9hnYzJKQ0LWJa0QARAQAB +AAP/TB81EIo2VYNmTq0pK1ZXwUpxCrvAAIG3hwKjEzHcbQznsjNvPUihZ+NZQ6+X +0HCfPAdPkGDCLCb6NavcSW+iNnLTrdDnSI6+3BbIONqWWdRDYJhqZCkqmG6zqSfL +IdkJgCw94taUg5BWP/AAeQrhzjChvpMQTVKQL5mnuZbUCeMCAN5qrYMP2S9iKdnk +VANIFj7656ARKt/nf4CBzxcpHTyB8+d2CtPDKCmlJP6vL8t58Jmih+kHJMvC0dzn +gr5f5+sCAOOe5gt9e0am7AvQWhdbHVfJU0TQJx+m2OiCJAqGTB1nvtBLHdJnfdC9 +TnXXQ6ZXibqLyBies/xeY2sCKL5qtTMCAKnX9+9d/5yQxRyrQUHt1NYhaXZnJbHx +q4ytu0eWz+5i68IYUSK69jJ1NWPM0T6SkqpB3KCAIv68VFm9PxqG1KmhSrQIVGVz +dCBLZXmIuAQTAQIAIgUCTasU2gIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA +CgkQO9o98PRieSoLhgQAkLEZex02Qt7vGhZzMwuN0R22w3VwyYyjBx+fM3JFETy1 +ut4xcLJoJfIaF5ZS38UplgakHG0FQ+b49i8dMij0aZmDqGxrew1m4kBfjXw9B/v+ +eIqpODryb6cOSwyQFH0lQkXC040pjq9YqDsO5w0WYNXYKDnzRV0p4H1pweo2VDid +AdgETasU2gEEAN46UPeWRqKHvA99arOxee38fBt2CI08iiWyI8T3J6ivtFGixSqV +bRcPxYO/qLpVe5l84Nb3X71GfVXlc9hyv7CD6tcowL59hg1E/DC5ydI8K8iEpUmK +/UnHdIY5h8/kqgGxkY/T/hgp5fRQgW1ZoZxLajVlMRZ8W4tFtT0DeA+JABEBAAEA +A/0bE1jaaZKj6ndqcw86jd+QtD1SF+Cf21CWRNeLKnUds4FRRvclzTyUMuWPkUeX +TaNNsUOFqBsf6QQ2oHUBBK4VCHffHCW4ZEX2cd6umz7mpHW6XzN4DECEzOVksXtc +lUC1j4UB91DC/RNQqwX1IV2QLSwssVotPMPqhOi0ZLNY7wIA3n7DWKInxYZZ4K+6 +rQ+POsz6brEoRHwr8x6XlHenq1Oki855pSa1yXIARoTrSJkBtn5oI+f8AzrnN0BN +oyeQAwIA/7E++3HDi5aweWrViiul9cd3rcsS0dEnksPhvS0ozCJiHsq/6GFmy7J8 +QSHZPteedBnZyNp5jR+H7cIfVN3KgwH/Skq4PsuPhDq5TKK6i8Pc1WW8MA6DXTdU +nLkX7RGmMwjC0DBf7KWAlPjFaONAX3a8ndnz//fy1q7u2l9AZwrj1qa1iJ8EGAEC +AAkFAk2rFNoCGwwACgkQO9o98PRieSo2/QP/WTzr4ioINVsvN1akKuekmEMI3LAp +BfHwatufxxP1U+3Si/6YIk7kuPB9Hs+pRqCXzbvPRrI8NHZBmc8qIGthishdCYad +AHcVnXjtxrULkQFGbGvhKURLvS9WnzD/m1K2zzwxzkPTzT9/Yf06O6Mal5AdugPL +VrM0m72/jnpKo04= +=zNCn +-----END PGP PRIVATE KEY BLOCK-----` + +const e2ePublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Charset: UTF-8 + +xv8AAABSBAAAAAATCCqGSM49AwEHAgME1LRoXSpOxtHXDUdmuvzchyg6005qIBJ4 +sfaSxX7QgH9RV2ONUhC+WiayCNADq+UMzuR/vunSr4aQffXvuGnR383/AAAAFDxk +Z2lsQHlhaG9vLWluYy5jb20+wv8AAACGBBATCAA4/wAAAAWCVGvAG/8AAAACiwn/ +AAAACZC2VkQCOjdvYf8AAAAFlQgJCgv/AAAAA5YBAv8AAAACngEAAE1BAP0X8veD +24IjmI5/C6ZAfVNXxgZZFhTAACFX75jUA3oD6AEAzoSwKf1aqH6oq62qhCN/pekX ++WAsVMBhNwzLpqtCRjLO/wAAAFYEAAAAABIIKoZIzj0DAQcCAwT50ain7vXiIRv8 +B1DO3x3cE/aattZ5sHNixJzRCXi2vQIA5QmOxZ6b5jjUekNbdHG3SZi1a2Ak5mfX +fRxC/5VGAwEIB8L/AAAAZQQYEwgAGP8AAAAFglRrwBz/AAAACZC2VkQCOjdvYQAA +FJAA9isX3xtGyMLYwp2F3nXm7QEdY5bq5VUcD/RJlj792VwA/1wH0pCzVLl4Q9F9 +ex7En5r7rHR5xwX82Msc+Rq9dSyO +=7MrZ +-----END PGP PUBLIC KEY BLOCK-----` + +const dsaKeyWithSHA512 = `9901a2044f04b07f110400db244efecc7316553ee08d179972aab87bb1214de7692593fcf5b6feb1c80fba268722dd464748539b85b81d574cd2d7ad0ca2444de4d849b8756bad7768c486c83a824f9bba4af773d11742bdfb4ac3b89ef8cc9452d4aad31a37e4b630d33927bff68e879284a1672659b8b298222fc68f370f3e24dccacc4a862442b9438b00a0ea444a24088dc23e26df7daf8f43cba3bffc4fe703fe3d6cd7fdca199d54ed8ae501c30e3ec7871ea9cdd4cf63cfe6fc82281d70a5b8bb493f922cd99fba5f088935596af087c8d818d5ec4d0b9afa7f070b3d7c1dd32a84fca08d8280b4890c8da1dde334de8e3cad8450eed2a4a4fcc2db7b8e5528b869a74a7f0189e11ef097ef1253582348de072bb07a9fa8ab838e993cef0ee203ff49298723e2d1f549b00559f886cd417a41692ce58d0ac1307dc71d85a8af21b0cf6eaa14baf2922d3a70389bedf17cc514ba0febbd107675a372fe84b90162a9e88b14d4b1c6be855b96b33fb198c46f058568817780435b6936167ebb3724b680f32bf27382ada2e37a879b3d9de2abe0c3f399350afd1ad438883f4791e2e3b4184453412068617368207472756e636174696f6e207465737488620413110a002205024f04b07f021b03060b090807030206150802090a0b0416020301021e01021780000a0910ef20e0cefca131581318009e2bf3bf047a44d75a9bacd00161ee04d435522397009a03a60d51bd8a568c6c021c8d7cf1be8d990d6417b0020003` + +const unknownHashFunctionHex = `8a00000040040001990006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` + +const rsaSignatureBadMPIlength = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` + +const missingHashFunctionHex = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` + +const campbellQuine = `a0b001000300fcffa0b001000d00f2ff000300fcffa0b001000d00f2ff8270a01c00000500faff8270a01c00000500faff000500faff001400ebff8270a01c00000500faff000500faff001400ebff428821c400001400ebff428821c400001400ebff428821c400001400ebff428821c400001400ebff428821c400000000ffff000000ffff000b00f4ff428821c400000000ffff000000ffff000b00f4ff0233214c40000100feff000233214c40000100feff0000` + +const keyV4forVerifyingSignedMessageV3 = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +mI0EVfxoFQEEAMBIqmbDfYygcvP6Phr1wr1XI41IF7Qixqybs/foBF8qqblD9gIY +BKpXjnBOtbkcVOJ0nljd3/sQIfH4E0vQwK5/4YRQSI59eKOqd6Fx+fWQOLG+uu6z +tewpeCj9LLHvibx/Sc7VWRnrznia6ftrXxJ/wHMezSab3tnGC0YPVdGNABEBAAG0 +JEdvY3J5cHRvIFRlc3QgS2V5IDx0aGVtYXhAZ21haWwuY29tPoi5BBMBCgAjBQJV +/GgVAhsDBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQeXnQmhdGW9PFVAP+ +K7TU0qX5ArvIONIxh/WAweyOk884c5cE8f+3NOPOOCRGyVy0FId5A7MmD5GOQh4H +JseOZVEVCqlmngEvtHZb3U1VYtVGE5WZ+6rQhGsMcWP5qaT4soYwMBlSYxgYwQcx +YhN9qOr292f9j2Y//TTIJmZT4Oa+lMxhWdqTfX+qMgG4jQRV/GgVAQQArhFSiij1 +b+hT3dnapbEU+23Z1yTu1DfF6zsxQ4XQWEV3eR8v+8mEDDNcz8oyyF56k6UQ3rXi +UMTIwRDg4V6SbZmaFbZYCOwp/EmXJ3rfhm7z7yzXj2OFN22luuqbyVhuL7LRdB0M +pxgmjXb4tTvfgKd26x34S+QqUJ7W6uprY4sAEQEAAYifBBgBCgAJBQJV/GgVAhsM +AAoJEHl50JoXRlvT7y8D/02ckx4OMkKBZo7viyrBw0MLG92i+DC2bs35PooHR6zz +786mitjOp5z2QWNLBvxC70S0qVfCIz8jKupO1J6rq6Z8CcbLF3qjm6h1omUBf8Nd +EfXKD2/2HV6zMKVknnKzIEzauh+eCKS2CeJUSSSryap/QLVAjRnckaES/OsEWhNB +=RZia +-----END PGP PUBLIC KEY BLOCK----- +` + +const signedMessageV3 = `-----BEGIN PGP MESSAGE----- +Comment: GPGTools - https://gpgtools.org + +owGbwMvMwMVYWXlhlrhb9GXG03JJDKF/MtxDMjKLFYAoUaEktbhEITe1uDgxPVWP +q5NhKjMrWAVcC9evD8z/bF/uWNjqtk/X3y5/38XGRQHm/57rrDRYuGnTw597Xqka +uM3137/hH3Os+Jf2dc0fXOITKwJvXJvecPVs0ta+Vg7ZO1MLn8w58Xx+6L58mbka +DGHyU9yTueZE8D+QF/Tz28Y78dqtF56R1VPn9Xw4uJqrWYdd7b3vIZ1V6R4Nh05d +iT57d/OhWwA= +=hG7R +-----END PGP MESSAGE----- +` + +const signedMessageOldStyle = `-----BEGIN PGP MESSAGE----- + +wsE7BAABCgBvBYJknZ5zCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmciNnrFGxZikAZXMAlYswJyEjuD4sHc2prQL+BM7agB +aRYhBNGmbhojsYLJmA94jPv8yCoBXnMwAABKUQv/XNqMZrPtp8w8oC+9dA8AxqDi +7Icq1pORiPXiTfcVhad0k2wI9c4Xt6u+ndJuaTgSouFkaFYjp1yiZfOdj/PwtdO2 +4MQSogOMyQT3jjw+lF+uK4zqFzxs+EzqTdPcMgn30/AXGPIggoQHid1/z//jbn5e +TUZB4cAE2RfBbQ0Yg/QS7PbkRS7P6hDrCKCcKYuQMQDUymGViounz0Sf4CBm++9x +1KyyXIHdtOFcSJZRH2nd2qM94kO++J8zxKt5yFaIctxOUPi6MdNhkCW4DuOtpqRF +xR4O3G6x1LcdNh3NzaiKUKmW+JY1PBWJaoL5Tfy9PwxttuuO8azEkAOFkRbz3yzT ++zLmIQIAsD5taXhJjmAlberhDIe6WUyYodKU8j61Z3kqD78QTD1xpqdAj2Xl1Dxk +PtdMR+yF8zOGn+X0xg08e/AEXDWMF7wcDj2XnHt+N+Xe1Bi9SNKTavsBPVuFDxSz +Rx1AJ2YkWexm44UdW9t7fQ4krEWPjG2Z9GzUg+dNxA0DAAoBzjJvjmOvI0EByxRi +AAAAAABIZWxsbw0KV29ybGQhCsLBOwQAAQoAbwWCZJ2ecwkQzjJvjmOvI0FHFAAA +AAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jnu6VKgSUjJVHfBIsa +u3WDOdUie4oCxfpRdKFJq6FDwp4WIQShwFEH+rDNIMZmfoDOMm+OY68jQQAA0bkL +/3zi/gB/VDRWdCKMHb8D+54y1JE2kAF6+s7ttx6C9eeaMtOOghD3rSeLWOGp1Ff9 +OFysla15+yXJTnc+/F0dO4dF1w3r4Y6gbJc36m5FVtNx3cuxsYPH0fu8GnkA9jHQ +FGHmOiUtCVjH98HwLZuCgJuSRuvzKvgONYnKYdy0inO3OSaYCPKlQvFL5S0vTHAJ +D7hq2Nld9Ac7iJIq0Wv5m36Qat32q11inMhUG+Qg45Eyc9dj0phy/J0dgfluNHZZ +rxHnNVSeWbhXk8blsckay05zbQs4uupLxHWy1VmxhULrpdkJSQ8s0uWVZz4kMOLM +eSPfEZRYu6+6eD6W9g3iLZ9sAq5lpcnGItPaaS7YMkjfR2y6lzhHCOAUf6dbTm8c +dJjpJlKdnJAhsth5MfLl+b94ZjYBWYhPIVTkuUuzK6bL46PywRGhx2v5JKoj2Sph +1imuaC76JixESDhKAM5GafpcmoymWuqJBB9lqxOMxfR3LDMaqwyfJUvM9vT2RMux +Lg== +=+yph +-----END PGP MESSAGE----- +` + +const signedMessageOldStyleKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +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 +=miES +-----END PGP PRIVATE KEY BLOCK----- +` + +// https://mailarchive.ietf.org/arch/msg/openpgp/9SheW_LENE0Kxf7haNllovPyAdY/ +const v5PrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +lGEFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd +fj75iux+my8QAAAAAAAiAQCHZ1SnSUmWqxEsoI6facIVZQu6mph3cBFzzTvcm5lA +Ng5ctBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0e8mHJGQC +X5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyICAQYVCgkI +CwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3wwJAXRJy9 +M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2BZxmBVyR9OQSAAAA +MgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0YvYWWAoD +AQgHAAAAAAAiAP9OdAPppjU1WwpqjIItkxr+VPQRT8Zm/Riw7U3F6v3OiBFHiHoF +GBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVAUCXJH05AIb +DAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijhob2U5AQC+RtOHCHx7 +TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw== +=IiS2 +-----END PGP PRIVATE KEY BLOCK-----` + +// See OpenPGP crypto refresh Section A.3. +const v6PrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB +exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ +BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh +RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe +7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/ +LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG +GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE +M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr +k0mXubZvyl4GBg== +-----END PGP PRIVATE KEY BLOCK-----` + +// See OpenPGP crypto refresh merge request: +// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/304 +const v6PrivKeyMsg = `-----BEGIN PGP MESSAGE----- + +wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO +WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS +aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l +yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo +bhF30A+IitsxxA== +-----END PGP MESSAGE-----` + +// See OpenPGP crypto refresh merge request: +// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/305 +const v6PrivKeyInlineSignMsg = `-----BEGIN PGP MESSAGE----- + +xEYGAQobIHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usyxhsTwYJppfk +1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkBy0p1AAAAAABXaGF0IHdlIG5lZWQgZnJv +bSB0aGUgZ3JvY2VyeSBzdG9yZToKCi0gdG9mdQotIHZlZ2V0YWJsZXMKLSBub29k +bGVzCsKYBgEbCgAAACkFgmOYo2MiIQbLGGxPBgmml+TVLfpscisMHx4nwYpWcI9l +JewnutmsyQAAAABpNiB2SV9QIYiQ9/Xi7jwYIlFPcFAPVR2G5ckh5ATjSlP7rCfQ +b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj +FtCgazStmsuOXF9SFQE= +-----END PGP MESSAGE-----` + +// See https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/274 +// decryption password: "correct horse battery staple" +const v6ArgonSealedPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC +FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS +3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC +Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW +cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin +7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/ +0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0 +gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf +9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR +v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr +DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki +Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt +ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG +-----END PGP PRIVATE KEY BLOCK-----` + +const v4Key25519 = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUkEZB3qzRto01j2k2pwN5ux9w70stPinAdXULLr20CRW7U7h2GSeACch0M+ +qzQg8yjFQ8VBvu3uwgKH9senoHmj72lLSCLTmhFKzQR0ZXN0wogEEBsIAD4F +gmQd6s0ECwkHCAmQIf45+TuC+xMDFQgKBBYAAgECGQECmwMCHgEWIQSWEzMi +jJUHvyIbVKIh/jn5O4L7EwAAUhaHNlgudvxARdPPETUzVgjuWi+YIz8w1xIb +lHQMvIrbe2sGCQIethpWofd0x7DHuv/ciHg+EoxJ/Td6h4pWtIoKx0kEZB3q +zRm4CyA7quliq7yx08AoOqHTuuCgvpkSdEhpp3pEyejQOgBo0p6ywIiLPllY +0t+jpNspHpAGfXID6oqjpYuJw3AfVRBlwnQEGBsIACoFgmQd6s0JkCH+Ofk7 +gvsTApsMFiEElhMzIoyVB78iG1SiIf45+TuC+xMAAGgQuN9G73446ykvJ/mL +sCZ7zGFId2gBd1EnG0FTC4npfOKpck0X8dngByrCxU8LDSfvjsEp/xDAiKsQ +aU71tdtNBQ== +=e7jT +-----END PGP PRIVATE KEY BLOCK-----` + +const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +hW1Hj9AO8lzggBQ= +=Nt+N +-----END PGP PUBLIC KEY BLOCK----- +` + +const sigFromKeyWithExpiredCrossSig = `-----BEGIN PGP SIGNATURE----- + +wsDzBAABCgAGBYJfLAFsACEJEHwvqk35PDeyFiEEHdzhXwkhfO4vOzdgfC+qTfk8 +N7KiqwwAts4QGB7v9bABCC2qkTxJhmStC0wQMcHRcjL/qAiVnmasQWmvE9KVsdm3 +AaXd8mIx4a37/RRvr9dYrY2eE4uw72cMqPxNja2tvVXkHQvk1oEUqfkvbXs4ypKI +NyeTWjXNOTZEbg0hbm3nMy+Wv7zgB1CEvAsEboLDJlhGqPcD+X8a6CJGrBGUBUrv +KVmZr3U6vEzClz3DBLpoddCQseJRhT4YM1nKmBlZ5quh2LFgTSpajv5OsZheqt9y +EZAPbqmLhDmWRQwGzkWHKceKS7nZ/ox2WK6OS7Ob8ZGZkM64iPo6/EGj5Yc19vQN +AGiIaPEGszBBWlOpHTPhNm0LB0nMWqqaT87oNYwP8CQuuxDb6rKJ2lffCmZH27Lb +UbQZcH8J+0UhpeaiadPZxH5ATJAcenmVtVVMLVOFnm+eIlxzov9ntpgGYt8hLdXB +ITEG9mMgp3TGS9ZzSifMZ8UGtHdp9QdBg8NEVPFzDOMGxpc/Bftav7RRRuPiAER+ +7A5CBid5 +=aQkm +-----END PGP SIGNATURE----- +` + +const signedMessageWithCriticalNotation = `-----BEGIN PGP MESSAGE----- + +owGbwMvMwMH4oOW7S46CznTG09xJDDE3Wl1KUotLuDousDAwcjBYiSmyXL+48d6x +U1PSGUxcj8IUszKBVMpMaWAAAgEGZpAeh9SKxNyCnFS95PzcytRiBi5OAZjyXXzM +f8WYLqv7TXP61Sa4rqT12CI3xaN73YS2pt089f96odCKaEPnWJ3iSGmzJaW/ug10 +2Zo8Wj2k4s7t8wt4H3HtTu+y5UZfV3VOO+l//sdE/o+Lsub8FZH7/eOq7OnbNp4n +vwjE8mqJXetNMfj8r2SCyvkEnlVRYR+/mnge+ib56FdJ8uKtqSxyvgA= +=fRXs +-----END PGP MESSAGE-----` + +const criticalNotationSigner = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+ +fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5 +GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0 +JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS +YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6 +AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki +Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf +9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rC4jQRSYS9OAQQA6R/PtBFa +JaT4jq10yqASk4sqwVMsc6HcifM5lSdxzExFP74naUMMyEsKHP53QxTF0Grqusag +Qg/ZtgT0CN1HUM152y7ACOdp1giKjpMzOTQClqCoclyvWOFB+L/SwGEIJf7LSCEr +woBuJifJc8xAVr0XX0JthoW+uP91eTQ3XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIb +LgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJSYS9OAAoJEOCE90RsICyXuqIEANmmiRCA +SF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhP +GLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2 +bZpsUxRTg9+cSrMWWSNjiY9qUKajm1tuzPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0X +W748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxqE9+QCEl7qinFqqBLjuzgUhBU4QlwX1GD +AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY +hz3tYjKhoFTKEIq3y3Pp +=h/aX +-----END PGP PUBLIC KEY BLOCK-----` + +const multiSignMessageKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +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 +=miES +-----END PGP PRIVATE KEY BLOCK-----` + +const multiSignMessage = `-----BEGIN PGP MESSAGE----- + +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 +xXuU4F38CnE/rCwvrXuKCfUeX3hUDl4TtRIg +=Du+Y +-----END PGP MESSAGE-----` + +const malformedLiteralSignature = `-----BEGIN PGP MESSAGE----- +Comment: Plaintext is "Hello World :)" +Comment: Encrypted using AES-256 +Comment: Session key: 4875CCE42B4BFCD2748EA4E1FD53E71B3C01ACC231CB5DDE6B33858B4DDE0788 + +wcDMA3wvqk35PDeyAQwA0GJCAQbgtF4YKAqNaHCAhkFtyvS0b6BFfPP5MT8uB7PA +MKgcl8kkYMyY0JO49HwPG/0jTWdfYrpP7hSwH1Ofq9HqAxJM4as138KAHi6TKwzg +Brn4KonZysttYUEhyqfkCsWzAhJfWKYaO2IG1W9K7nxvL3WAFjB70/SX3nJqitnw +GbeBhRJerlq9f4QsIfz9eiQClYn5Cy1VWORO4BpgvQpKMaY6mBGPcycynycQlnhX +vKS5I04zOepTGdUko7koo8ebh7TrtcYXOjFRy+VJvloU3VYoGOjZ9LrQcHbt7mQi +HOmMHxdXH0b3WwGq3XrDYErT42RQGxA1iVyb6gZpWg7aNByYsQYfymdEl18stULb +26+mdrma0x3WMQ2YHrgJcLAv+qou4iE3Y3c8CpP+qDVJiSid1BhDvcOXP3W74GQ5 +PVtx1VWCxYMBaSbGXLFMXF4bD24UQiw2maq1pEWOjdIZB7B3jxY0Pd3nZWB9zraS +8LeRG+eXmQHXf7lyC3GX0sF9ASHRiWi+o7ptTb47rTTp7fQDbS07oRyceuQKakTW +cf+//2s3FAiWK3eUQclGSuXCd+tXPGbHnNtBxkEfV5cMeSc9E1pCYJKFbhw6w2Lc +/wJEoPMUihe/gD7tlir9NUgt4IoS3WZ36lQ8/kpMWBBWhM9Fa/3X6n/M+2cU41eE +7BqXhyWlv5WDbgWhOtBAkpziWoBKrzvZUy8pnpGtpxo3r1hntjerYck7OC1FtlRI +uUOZRObvb0F1YvdpQu+DWHMKHz9JgZf0M37hnMJWgU63+NZGTdZlnOfiQPwB6z0e +y5RkaeuL6EASrT75qrjo545ahwu8UKJnS0MiU9sceN8T0YaKHI2tV2ujWL7B8PEP +ik4JX1ieB0mb1RKRPmuG/b5TPmFCxS3cxr8MXfEbuSrfk0rmi4NGhuAnXN2FeXQr +kc+qLfMA4RPUAPez3mBIrggB/FCu4b+LoQlVy5XJSrtqJZdzDZ31K65xdFkPTto3 +CtYAqAtqb0nbOknK5YgmkdOHbECWA1umrrwyU8d5gYfweccycpaKVl2q3atNipRC +okOBWGJ+5mvim7K3fHmDaClMi3X2cnUIG1c+SEM1MAUHJpbCVLRH6PETYNh4iMx7 +WwVgUNP+BbQChmoTgGESm23MquSle3RclvSfXDIzcH2pwHc9gafK8Eau4A/U7sFH +s/ikCGhW85qxUxdWdSvJ3KJ5Clxz62vLWrBi1Z0qdefZhDmVzPv+9UAb2+drLsyz +WxvpR7aTncLHHiJel8AT +=B+9z +-----END PGP MESSAGE----- +` + +const malformedOPSLiteral = `-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQwAuwIwz2aT6DILqIsAZhlPdmg4MPagOMB3vSpp16cZx2Fu +UMoM8Mw2Iv6JLUg9MGZ/ngN6Fciq+F1+D+8iycznah/OJ8cL7YhSFbGCM4PtcumY +/xMafd7VH89LW+Z0lBmPl82HIYACFseUG/T+xPYgEYyQ+5LL1EGNf5EWYzmiwrUy +BV3cnpiAhTHwl28RttoFCyHlFi3t37rUu12oUvbamPd0Hr3PPy1M48RZFaN4YlzS +1MS2vchFOwaCWrdobX2/b/F/S4q5KLh+T/nIjOakEKWX6LaDwNf6UmQjlr3nO1jO +BVzY5ki8qgUUbEZXklYx+Z5UariZLXyS5OqoNkjbmT8uUlWzk2M3nlBalVDCJOCb +mV9I2XjLxY+2m39LOCHP2KtkKSLqFAEYnl7zI9/Jlh0NwqMfxjH7CKphItTXan3D +CfYBn5fhZKuLDDSJy2FDgE4u3yEixUP484PY9nJkVEzelszvfKh+2mEG9AyCGlul +XfAoNVmFSeOnzWwofFC10k4BngbmDnzcsGj0lNK/HVGjNc6I5eUxvxE3ctpfTVxu +xsrcHkZ/WTIUtgTVOmFs4voLGHF4X24Dba4Ky16FWj+OVI+0HAY/dxF5hgZGTiU= +=GGy+ +-----END PGP MESSAGE----- + +` + +const malformedTwoLiteralsCompressed = `-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQwAg3lhuA+osFEHny7ymoJxb9uM0SMysKPHB3aUkpFYWeWx +5F8qsk6dnK2yJ1KK4R2MGHustgusMPmZ8xzyDDQ45+PiCrDXDLemikg4eR/Ypi4g +mrExbL4dNleGgyNNjoLLbXQsK/invXe4gxio238LVR2sEmHUSZiuCrZMKhy8ZVX3 +2bmMBhLWeCOjUkbIb0d6vkRRQay2v+pmH9lJm4Exm9ZIHQKJEV8TgPd0HI4RI1T7 +RdPIqzts8gwEloItBEymRwP3nwx9d4wAZg6S75fQo3Zy4qnLIGqo/LLjIlzyKmed +wNaOoJfNbTyL5Hqt2isqd2wMWv7h638BACDrbBZF6PXpzZpGZ+Ay0CUdcq3gM00A +52hHNqIkhcHZDInRB+aHrfCxYYpNsvou08rcJrGPhOue6hXz2NCb/aGMueIVF6/G +OGBa5/qtsWe6cz/iUMOi9xG7XoCXfwPs/6h74NmzwsDwG+OCZpw0WfJET6liEpJM +yJf5tj3ur3Eg0FCgd20x0lcBVhzPjawsmdCxGvUNW8379EqcM4ZYB18djDh5MO/6 +u652ZZclXDlpXHpoRDH+fkTwKO5gkukOXyRAlqpctTq0kOMYD5Dc6ByhU9TMWEpK +kKBxYwHkxJo= +=GV4u +-----END PGP MESSAGE----- +` + +const malformedPKESKLiteral = `-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv/Wt7MVNdahLe8iT/kE4REc7YAE8+SSk+0l4aMWxwsuu2X +pQm+y8YNNW8QqgkSDBK2xXuYGixCH6qTa+1gtc1kN5+dnT93zJLfGXjvSucfKcau +YRNlwe6B4oQ9GHqNEQHalXJhxlxgrlrA8esrBtKNWVfR5dzB7MxbWdxwOi6pvAft ++pt6fz4+5AGffGTXpKNqpjnil+XA2JYEnhry/GKDPONmK1mk4dWMfUcLac77w+Vm +wIfviqDyv6urJy1FK8Eg/FzOL9hosPZxDTdHeF+DM2HwxQWtftRs4nmGFkYyT9Yx +N+T6Dz1sVYMtHNa0tQLJ4VTzgPY7cc34ShMIi0FDUYa83KqgBufdoH4UlqGkljTe +bT1I5txQQpO7wsrx0pxnPqRjlw0dabjyF2oxWSmMVGA76Htd/hW5PQ56g/smpwf8 +DLFATxdt8boGUMwDC0/PftH+c3FfYjGj24uesC14BS+S8HW4+uLLAoQixraRpJ9v +UGrrBw2oDHaFnJIPQM240sEOAUVlV7k/hCnyDlk4tBHDcXUoekojLbncW2x+lY26 +/VX6+gka96sWyNgVzFaoGf6OYVhtpvXEhECXKYP5sf3a4Gn+bQSZgKr2tf9N9l7s +L3akqkorBWPHWnsnrV/Iuiv7SyALLVPjAJcGLfCx4/MClDOA4XZfz4pEzW6fyzKg +L1yknBIpx9vUSQ4Sb1dptuvd79pUXfgK+xfNentShz34lge49vS3+pZ1WAbvf7cg +wZRnHRxCTO2qOACEKoz+lG2RFfzp+Tf4qbEaCsBFvjT7GCjLyZZbaUqfuB4mSpid +KUwS4jWFDrtGSE1CTDyp4QL5HDY6SNegY5iy2dud1RJrCvWgJtGWBdqOLnnvvH9t +aJZn7rW+8VReLNP4XuAHVbVNEIXMPVZ/A1noZHSzEVSwIhuzH7ZrAJt6ca/DJoID +K+6+WlNsPHg9ItcM1vMKTqwEvw8f7t2khQbLJRNAESp9Bh8Ruphb80txanI7PZCS +niGt565u5focQLyRCuCc2m5eg34dpCVnXGCI3XSMAg2rq9iIxBrp0jj+jpitO+Bq +Li+3mrVJktp74N+ocjr54FZMd4rWxev317MUi9UB6LeDLRhrvRsucAmypLpkkGuU +=9ciZ +-----END PGP MESSAGE----- +` + +const malformedKeyTest = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +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 +=miES +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/openpgp/v2/subkeys.go b/openpgp/v2/subkeys.go new file mode 100644 index 000000000..a1435e14e --- /dev/null +++ b/openpgp/v2/subkeys.go @@ -0,0 +1,207 @@ +package v2 + +import ( + "io" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// Subkey is an additional public key in an Entity. Subkeys can be used for +// encryption. +type Subkey struct { + Primary *Entity + PublicKey *packet.PublicKey + PrivateKey *packet.PrivateKey + Bindings []*packet.VerifiableSignature + Revocations []*packet.VerifiableSignature +} + +func readSubkey(primary *Entity, packets *packet.Reader, pub *packet.PublicKey, priv *packet.PrivateKey) error { + subKey := Subkey{ + PublicKey: pub, + PrivateKey: priv, + Primary: primary, + } + + for { + p, err := packets.Next() + if err == io.EOF { + break + } else if err != nil { + return errors.StructuralError("subkey signature invalid: " + err.Error()) + } + + sig, ok := p.(*packet.Signature) + if !ok { + packets.Unread(p) + break + } + + if sig.SigType != packet.SigTypeSubkeyBinding && sig.SigType != packet.SigTypeSubkeyRevocation { + // Ignore signatures with wrong type + continue + } + switch sig.SigType { + case packet.SigTypeSubkeyRevocation: + subKey.Revocations = append(subKey.Revocations, packet.NewVerifiableSig(sig)) + case packet.SigTypeSubkeyBinding: + subKey.Bindings = append(subKey.Bindings, packet.NewVerifiableSig(sig)) + } + } + primary.Subkeys = append(primary.Subkeys, subKey) + return nil +} + +// Serialize serializes the subkey and writes it into writer. +// The includeSecrets flag controls if the secrets should be included in the encoding or not. +func (s *Subkey) Serialize(w io.Writer, includeSecrets bool) error { + if includeSecrets { + if err := s.PrivateKey.Serialize(w); err != nil { + return err + } + } else { + if err := s.PublicKey.Serialize(w); err != nil { + return err + } + } + for _, revocation := range s.Revocations { + if err := revocation.Packet.Serialize(w); err != nil { + return err + } + } + for _, bindingSig := range s.Bindings { + if err := bindingSig.Packet.Serialize(w); err != nil { + return err + } + } + return nil +} + +// ReSign resigns the latest valid subkey binding signature with the given config. +func (s *Subkey) ReSign(config *packet.Config) error { + selectedSig, err := s.LatestValidBindingSignature(time.Time{}) + if err != nil { + return err + } + err = selectedSig.SignKey(s.PublicKey, s.Primary.PrivateKey, config) + if err != nil { + return err + } + if selectedSig.EmbeddedSignature != nil { + err = selectedSig.EmbeddedSignature.CrossSignKey(s.PublicKey, s.Primary.PrimaryKey, + s.PrivateKey, config) + if err != nil { + return err + } + } + return nil +} + +// Verify checks if the subkey is valid by checking: +// - that the key is not revoked +// - that there is valid non-expired binding self-signature +// - that the subkey is not expired +// If date is zero (i.e., date.IsZero() == true) the time checks are not performed. +func (s *Subkey) Verify(date time.Time) (selfSig *packet.Signature, err error) { + selfSig, err = s.LatestValidBindingSignature(date) + if err != nil { + return nil, err + } + if s.Revoked(selfSig, date) { + return nil, errors.ErrKeyRevoked + } + if !date.IsZero() && s.Expired(selfSig, date) { + return nil, errors.ErrKeyExpired + } + return +} + +// Expired checks if given the selected self-signature if the subkey is expired. +func (s *Subkey) Expired(selectedSig *packet.Signature, date time.Time) bool { + return s.PublicKey.KeyExpired(selectedSig, date) || selectedSig.SigExpired(date) +} + +// Revoked returns whether the subkey has been revoked by a self-signature. +// Note that third-party revocation signatures are not supported. +func (s *Subkey) Revoked(selfCertification *packet.Signature, date time.Time) bool { + // Verify revocations first + for _, revocation := range s.Revocations { + if revocation.Valid == nil { + err := s.Primary.PrimaryKey.VerifySubkeyRevocationSignature(revocation.Packet, s.PublicKey) + valid := err == nil + revocation.Valid = &valid + } + if *revocation.Valid && + (revocation.Packet.RevocationReason == nil || + *revocation.Packet.RevocationReason == packet.Unknown || + *revocation.Packet.RevocationReason == packet.NoReason || + *revocation.Packet.RevocationReason == packet.KeyCompromised) { + // If the key is compromised, the key is considered revoked even before the revocation date. + return true + } + if *revocation.Valid && (date.IsZero() || + !revocation.Packet.SigExpired(date) && + (selfCertification == nil || + selfCertification.CreationTime.Unix() <= revocation.Packet.CreationTime.Unix())) { + return true + } + } + return false +} + +// Revoke generates a subkey revocation signature (packet.SigTypeSubkeyRevocation) for +// a subkey with the specified reason code and text (RFC4880 section-5.2.3.23). +// If config is nil, sensible defaults will be used. +func (s *Subkey) Revoke(reason packet.ReasonForRevocation, reasonText string, config *packet.Config) error { + // Check that the subkey is valid (not considering expiration) + if _, err := s.Verify(time.Time{}); err != nil { + return err + } + + revSig := createSignaturePacket(s.Primary.PrimaryKey, packet.SigTypeSubkeyRevocation, config) + revSig.RevocationReason = &reason + revSig.RevocationReasonText = reasonText + + if err := revSig.RevokeSubkey(s.PublicKey, s.Primary.PrivateKey, config); err != nil { + return err + } + sig := packet.NewVerifiableSig(revSig) + valid := true + sig.Valid = &valid + s.Revocations = append(s.Revocations, sig) + return nil +} + +// LatestValidBindingSignature returns the latest valid self-signature of this subkey +// that is not newer than the provided date. +// Does not consider signatures/embedded signatures that are expired. +// If date is zero (i.e., date.IsZero() == true) the expiration checks are not performed. +// Returns a StructuralError if no valid self-signature is found. +func (s *Subkey) LatestValidBindingSignature(date time.Time) (selectedSig *packet.Signature, err error) { + for sigIdx := len(s.Bindings) - 1; sigIdx >= 0; sigIdx-- { + sig := s.Bindings[sigIdx] + if (date.IsZero() || date.Unix() >= sig.Packet.CreationTime.Unix()) && + (selectedSig == nil || selectedSig.CreationTime.Unix() < sig.Packet.CreationTime.Unix()) { + if sig.Valid == nil { + err := s.Primary.PrimaryKey.VerifyKeySignature(s.PublicKey, sig.Packet) + valid := err == nil + sig.Valid = &valid + } + mainSigExpired := !date.IsZero() && + sig.Packet.SigExpired(date) + embeddedSigExpired := !date.IsZero() && + sig.Packet.FlagSign && + sig.Packet.EmbeddedSignature != nil && + sig.Packet.EmbeddedSignature.SigExpired(date) + if *sig.Valid && !mainSigExpired && !embeddedSigExpired { + selectedSig = sig.Packet + } + } + } + if selectedSig == nil { + return nil, errors.StructuralError("no valid binding signature found for subkey") + } + return +} diff --git a/openpgp/v2/user.go b/openpgp/v2/user.go new file mode 100644 index 000000000..10f5199a6 --- /dev/null +++ b/openpgp/v2/user.go @@ -0,0 +1,217 @@ +package v2 + +import ( + "io" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// Identity represents an identity claimed by an Entity and zero or more +// assertions by other entities about that claim. +type Identity struct { + Primary *Entity + Name string // by convention, has the form "Full Name (comment) " + UserId *packet.UserId + SelfCertifications []*packet.VerifiableSignature + OtherCertifications []*packet.VerifiableSignature + Revocations []*packet.VerifiableSignature +} + +func readUser(e *Entity, packets *packet.Reader, pkt *packet.UserId) error { + identity := Identity{ + Primary: e, + Name: pkt.Id, + UserId: pkt, + } + for { + p, err := packets.NextWithUnsupported() + if err == io.EOF { + break + } else if err != nil { + return err + } + + unsupportedPacket, unsupported := p.(*packet.UnsupportedPacket) + sigCandidate := p + if unsupported { + sigCandidate = unsupportedPacket.IncompletePacket + } + sig, ok := sigCandidate.(*packet.Signature) + if !ok { + // sigCandidate is a not a signature packet, reset and stop. + packets.Unread(p) + break + } else if unsupported { + // sigCandidate is a signature packet but unsupported. + continue + } + + if sig.SigType != packet.SigTypeGenericCert && + sig.SigType != packet.SigTypePersonaCert && + sig.SigType != packet.SigTypeCasualCert && + sig.SigType != packet.SigTypePositiveCert && + sig.SigType != packet.SigTypeCertificationRevocation { + // Ignore signatures with wrong type + continue + } + + if sig.CheckKeyIdOrFingerprint(e.PrimaryKey) { + if sig.SigType == packet.SigTypeCertificationRevocation { + identity.Revocations = append(identity.Revocations, packet.NewVerifiableSig(sig)) + } else { + identity.SelfCertifications = append(identity.SelfCertifications, packet.NewVerifiableSig(sig)) + } + e.Identities[pkt.Id] = &identity + } else { + identity.OtherCertifications = append(identity.OtherCertifications, packet.NewVerifiableSig(sig)) + } + } + return nil +} + +// Serialize serializes the user id to the writer. +func (i *Identity) Serialize(w io.Writer) error { + if err := i.UserId.Serialize(w); err != nil { + return err + } + for _, sig := range i.Revocations { + if err := sig.Packet.Serialize(w); err != nil { + return err + } + } + for _, sig := range i.SelfCertifications { + if err := sig.Packet.Serialize(w); err != nil { + return err + } + } + for _, sig := range i.OtherCertifications { + if err := sig.Packet.Serialize(w); err != nil { + return err + } + } + return nil +} + +// Verify checks if the user-id is valid by checking: +// - that a valid self-certification exists and is not expired +// - that user-id has not been revoked at the given point in time +// If date is zero (i.e., date.IsZero() == true) the time checks are not performed. +func (i *Identity) Verify(date time.Time) (selfSignature *packet.Signature, err error) { + if selfSignature, err = i.LatestValidSelfCertification(date); err != nil { + return + } + if i.Revoked(selfSignature, date) { + return nil, errors.StructuralError("user-id is revoked") + } + return +} + +// Revoked returns whether the identity has been revoked by a self-signature. +// Note that third-party revocation signatures are not supported. +func (i *Identity) Revoked(selfCertification *packet.Signature, date time.Time) bool { + // Verify revocations first + for _, revocation := range i.Revocations { + if selfCertification == nil || // if there is not selfCertification verify revocation + selfCertification.IssuerKeyId == nil || + revocation.Packet.IssuerKeyId == nil || + (*selfCertification.IssuerKeyId == *revocation.Packet.IssuerKeyId) { // check matching key id + if revocation.Valid == nil { + // Verify revocation signature (not verified yet). + err := i.Primary.PrimaryKey.VerifyUserIdSignature(i.Name, i.Primary.PrimaryKey, revocation.Packet) + valid := err == nil + revocation.Valid = &valid + } + + if *revocation.Valid && + (date.IsZero() || // Check revocation not expired + !revocation.Packet.SigExpired(date)) && + (selfCertification == nil || // Check that revocation is not older than the selfCertification + selfCertification.CreationTime.Unix() <= revocation.Packet.CreationTime.Unix()) { + return true + } + } + } + return false +} + +// ReSign resigns the latest valid self-certification with the given config. +func (i *Identity) ReSign(config *packet.Config) error { + selectedSig, err := i.LatestValidSelfCertification(config.Now()) + if err != nil { + return err + } + if err = selectedSig.SignUserId( + i.UserId.Id, + i.Primary.PrimaryKey, + i.Primary.PrivateKey, + config, + ); err != nil { + return err + } + return nil +} + +// SignIdentity adds a signature to e, from signer, attesting that identity is +// associated with e. The provided identity must already be an element of +// e.Identities and the private key of signer must have been decrypted if +// necessary. +// If config is nil, sensible defaults will be used. +func (ident *Identity) SignIdentity(signer *Entity, config *packet.Config) error { + certificationKey, ok := signer.CertificationKey(config.Now(), config) + if !ok { + return errors.InvalidArgumentError("no valid certification key found") + } + + if certificationKey.PrivateKey.Encrypted { + return errors.InvalidArgumentError("signing Entity's private key must be decrypted") + } + + if !ok { + return errors.InvalidArgumentError("given identity string not found in Entity") + } + + sig := createSignaturePacket(certificationKey.PublicKey, packet.SigTypeGenericCert, config) + + signingUserID := config.SigningUserId() + if signingUserID != "" { + if _, ok := signer.Identities[signingUserID]; !ok { + return errors.InvalidArgumentError("signer identity string not found in signer Entity") + } + sig.SignerUserId = &signingUserID + } + + if err := sig.SignUserId(ident.Name, ident.Primary.PrimaryKey, certificationKey.PrivateKey, config); err != nil { + return err + } + ident.OtherCertifications = append(ident.OtherCertifications, packet.NewVerifiableSig(sig)) + return nil +} + +// LatestValidSelfCertification returns the latest valid self-signature of this user-id +// that is not newer than the provided date. +// Does not consider signatures that are expired. +// If date is zero (i.e., date.IsZero() == true) the expiration checks are not performed. +// Returns a StructuralError if no valid self-certification is found. +func (i *Identity) LatestValidSelfCertification(date time.Time) (selectedSig *packet.Signature, err error) { + for sigIdx := len(i.SelfCertifications) - 1; sigIdx >= 0; sigIdx-- { + sig := i.SelfCertifications[sigIdx] + if (date.IsZero() || date.Unix() >= sig.Packet.CreationTime.Unix()) && // SelfCertification must be older than date + (selectedSig == nil || selectedSig.CreationTime.Unix() < sig.Packet.CreationTime.Unix()) { // Newer ones are preferred + if sig.Valid == nil { + // Verify revocation signature (not verified yet). + err = i.Primary.PrimaryKey.VerifyUserIdSignature(i.Name, i.Primary.PrimaryKey, sig.Packet) + valid := err == nil + sig.Valid = &valid + } + if *sig.Valid && (date.IsZero() || !sig.Packet.SigExpired(date)) { + selectedSig = sig.Packet + } + } + } + if selectedSig == nil { + return nil, errors.StructuralError("no valid certification signature found for identity") + } + return +} diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go new file mode 100644 index 000000000..4c02e04a9 --- /dev/null +++ b/openpgp/v2/write.go @@ -0,0 +1,1053 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + "bytes" + "crypto" + goerrors "errors" + "hash" + "io" + "strconv" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// DetachSign signs message with the private key from signer (which must +// already have been decrypted) and writes the signature to w. +// If config is nil, sensible defaults will be used. +func DetachSign(w io.Writer, signers []*Entity, message io.Reader, config *packet.Config) error { + return detachSign(w, signers, message, packet.SigTypeBinary, config) +} + +// DetachSignWithParams signs message with the private key from signer (which must +// already have been decrypted) and writes the signature to the Writer. +// If config is nil, sensible defaults will be used. +func DetachSignWithParams(w io.Writer, signers []*Entity, message io.Reader, params *SignParams) error { + if params == nil { + params = &SignParams{} + } + sigType := packet.SigTypeBinary + if params.TextSig { + sigType = packet.SigTypeText + } + return detachSign(w, signers, message, sigType, params.Config) +} + +// ArmoredDetachSign signs message with the private key from signer (which +// must already have been decrypted) and writes an armored signature to the Writer. +// If config is nil, sensible defaults will be used. +func ArmoredDetachSign(w io.Writer, signers []*Entity, message io.Reader, params *SignParams) (err error) { + if params == nil { + params = &SignParams{} + } + sigType := packet.SigTypeBinary + if params.TextSig { + sigType = packet.SigTypeText + } + return armoredDetachSign(w, signers, message, sigType, params.Config) +} + +// DetachSignWriter signs a message with the private key from a signer (which must +// already have been decrypted) and writes the signature to the Writer. +// DetachSignWriter returns a WriteCloser to which the message can be written to. +// The resulting WriteCloser must be closed after the contents of the message have +// been written. If utf8Message is set to true, the line endings of the message are +// canonicalised and the type of the signature will be SigTypeText. +// If config is nil, sensible defaults will be used. +func DetachSignWriter(w io.Writer, signers []*Entity, params *SignParams) (io.WriteCloser, error) { + if params == nil { + params = &SignParams{} + } + sigType := packet.SigTypeBinary + if params.TextSig { + sigType = packet.SigTypeText + } + return detachSignWithWriter(w, signers, sigType, params.Config) +} + +func armoredDetachSign(w io.Writer, signers []*Entity, message io.Reader, sigType packet.SignatureType, config *packet.Config) (err error) { + out, err := armor.EncodeWithChecksumOption(w, SignatureType, nil, false) + if err != nil { + return + } + err = detachSign(out, signers, message, sigType, config) + if err != nil { + return + } + return out.Close() +} + +func detachSign(w io.Writer, signers []*Entity, message io.Reader, sigType packet.SignatureType, config *packet.Config) (err error) { + ptWriter, err := detachSignWithWriter(w, signers, sigType, config) + if err != nil { + return + } + _, err = io.Copy(ptWriter, message) + if err != nil { + return + } + return ptWriter.Close() +} + +type detachSignWriter struct { + signatureWriter io.Writer + signatures []*detachSignContext + config *packet.Config +} + +type detachSignContext struct { + wrappedHash hash.Hash + h hash.Hash + signer *packet.PrivateKey + sig *packet.Signature +} + +func (s detachSignWriter) Write(data []byte) (int, error) { + for _, signature := range s.signatures { + if n, err := signature.wrappedHash.Write(data); err != nil { + return n, err + } + } + return len(data), nil +} + +func (s detachSignWriter) Close() error { + for _, signature := range s.signatures { + err := signature.sig.Sign(signature.h, signature.signer, s.config) + if err != nil { + return err + } + err = signature.sig.Serialize(s.signatureWriter) + if err != nil { + return err + } + } + return nil +} + +func detachSignWithWriter(w io.Writer, signers []*Entity, sigType packet.SignatureType, config *packet.Config) (ptWriter io.WriteCloser, err error) { + var detachSignContexts []*detachSignContext + for _, signer := range signers { + signingKey, ok := signer.SigningKeyById(config.Now(), config.SigningKey(), config) + if !ok { + return nil, errors.InvalidArgumentError("no valid signing keys") + } + if signingKey.PrivateKey == nil { + return nil, errors.InvalidArgumentError("signing key doesn't have a private key") + } + if signingKey.PrivateKey.Encrypted { + return nil, errors.InvalidArgumentError("signing key is encrypted") + } + candidateHashes := []uint8{ + hashToHashId(crypto.SHA256), + hashToHashId(crypto.SHA384), + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), + } + defaultHashes := candidateHashes[0:1] + primarySelfSignature, _ := signer.PrimarySelfSignature(config.Now()) + if primarySelfSignature == nil { + return nil, errors.StructuralError("signed entity has no valid self-signature") + } + preferredHashes := primarySelfSignature.PreferredHash + if len(preferredHashes) == 0 { + preferredHashes = defaultHashes + } + candidateHashes = intersectPreferences(candidateHashes, preferredHashes) + + var hash crypto.Hash + if hash, err = selectHash(candidateHashes, config.Hash(), signingKey.PrivateKey); err != nil { + return + } + + detachSignCtx := detachSignContext{ + signer: signingKey.PrivateKey, + } + + detachSignCtx.sig = createSignaturePacket(signingKey.PublicKey, sigType, config) + detachSignCtx.sig.Hash = hash + + detachSignCtx.h, err = detachSignCtx.sig.PrepareSign(config) + if err != nil { + return + } + detachSignCtx.wrappedHash, err = wrapHashForSignature(detachSignCtx.h, sigType) + if err != nil { + return + } + detachSignContexts = append(detachSignContexts, &detachSignCtx) + } + + return &detachSignWriter{ + signatureWriter: w, + signatures: detachSignContexts, + config: config, + }, nil +} + +// FileHints contains metadata about encrypted files. This metadata is, itself, +// encrypted. OpenPGP signatures do not include the FileHints in a signature hash and +// thus those fields are not protected against tampering in a signed document. +// The crypto[refresh does not recommend to set the data in file hints. +// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#section-5.9. +type FileHints struct { + // IsUTF8 can be set to hint that the contents are utf8 encoded data. + IsUTF8 bool + // FileName hints at the name of the file that should be written. + FileName string + // ModTime contains the modification time of the file, or the zero time if not applicable. + ModTime time.Time +} + +type EncryptParams struct { + // KeyWriter is a Writer to which the encrypted + // session keys are written to. + // If nil, DataWriter is used instead. + KeyWriter io.Writer + // Hints contains file metadata for the literal data packet. + // If nil, default is used. + Hints *FileHints + // SiningEntities contains the private keys to produce signatures with + // If nil, no signatures are created. + Signers []*Entity + // TextSig indicates if signatures of type SigTypeText should be produced. + TextSig bool + // Passwords defines additional passwords that the message should be encrypted to. + // i.e., for each defined password an additional SKESK packet is written. + Passwords [][]byte + // SessionKey provides a session key to be used for encryption. + // If nil, a random one-time session key is generated. + SessionKey []byte + // OutsideSig allows to set a signature that should be included + // in the message to encrypt. + // Should only be used for exceptional cases. + // If nil, ignored. + OutsideSig []byte + // EncryptionTime allows to override the time that is used + // for selecting the encryption key. + // If EncryptionTime is zero (i.e., EncryptionTime.isZero()) expiration checks + // are not performed on the encryption key. + // If nil, the default clock in config is used. + EncryptionTime *time.Time + // Config provides the config to be used. + // If Config is nil, sensible defaults will be used. + Config *packet.Config +} + +// SymmetricallyEncrypt acts like gpg -c: it encrypts a file with a passphrase. +// The resulting WriteCloser must be closed after the contents of the file have +// been written. +// If config is nil, sensible defaults will be used. +func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) { + return SymmetricallyEncryptWithParams(passphrase, ciphertext, &EncryptParams{ + Hints: hints, + Config: config, + }) +} + +// SymmetricallyEncryptWithParams acts like SymmetricallyEncrypt but provides more configuration options. +// EncryptParams provides the optional parameters. +// The resulting WriteCloser must be closed after the contents of the file have been written. +func SymmetricallyEncryptWithParams(passphrase []byte, dataWriter io.Writer, params *EncryptParams) (plaintext io.WriteCloser, err error) { + if params == nil { + params = &EncryptParams{} + } + return symmetricallyEncrypt(passphrase, dataWriter, params) +} + +func symmetricallyEncrypt(passphrase []byte, dataWriter io.Writer, params *EncryptParams) (plaintext io.WriteCloser, err error) { + if params.KeyWriter == nil { + params.KeyWriter = dataWriter + } + if params.Hints == nil { + params.Hints = &FileHints{} + } + if params.SessionKey == nil { + params.SessionKey, err = packet.SerializeSymmetricKeyEncrypted(params.KeyWriter, passphrase, params.Config) + defer func() { + // zero the session key after we are done + for i := range params.SessionKey { + params.SessionKey[i] = 0 + } + params.SessionKey = nil + }() + } else { + err = packet.SerializeSymmetricKeyEncryptedReuseKey(params.KeyWriter, params.SessionKey, passphrase, params.Config) + } + if err != nil { + return + } + for _, additionalPassword := range params.Passwords { + if err = packet.SerializeSymmetricKeyEncryptedReuseKey(params.KeyWriter, params.SessionKey, additionalPassword, params.Config); err != nil { + return + } + } + + config := params.Config + candidateCompression := []uint8{uint8(config.Compression())} + cipherSuite := packet.CipherSuite{ + Cipher: config.Cipher(), + Mode: config.AEAD().Mode(), + } + var candidateHashesPerSignature [][]uint8 + if params.Signers != nil { + for _, signer := range params.Signers { + // candidateHashes := []uint8{hashToHashId(config.Hash())} + // Check what the preferred hashes are for the signing key + candidateHashes := []uint8{ + hashToHashId(crypto.SHA256), + hashToHashId(crypto.SHA384), + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), + } + defaultHashes := candidateHashes[0:1] + primarySelfSignature, _ := signer.PrimarySelfSignature(params.Config.Now()) + if primarySelfSignature == nil { + return nil, errors.StructuralError("signed entity has no self-signature") + } + preferredHashes := primarySelfSignature.PreferredHash + if len(preferredHashes) == 0 { + preferredHashes = defaultHashes + } + candidateHashes = intersectPreferences(candidateHashes, preferredHashes) + if len(candidateHashes) == 0 { + candidateHashes = []uint8{hashToHashId(crypto.SHA256)} + } + candidateHashesPerSignature = append(candidateHashesPerSignature, candidateHashes) + } + } + return encryptDataAndSign(dataWriter, params, candidateHashesPerSignature, candidateCompression, config.Cipher(), config.AEAD() != nil, cipherSuite, nil) +} + +// intersectPreferences mutates and returns a prefix of a that contains only +// the values in the intersection of a and b. The order of a is preserved. +func intersectPreferences(a []uint8, b []uint8) (intersection []uint8) { + var j int + for _, v := range a { + for _, v2 := range b { + if v == v2 { + a[j] = v + j++ + break + } + } + } + + return a[:j] +} + +// intersectCipherSuites mutates and returns a prefix of a that contains only +// the values in the intersection of a and b. The order of a is preserved. +func intersectCipherSuites(a [][2]uint8, b [][2]uint8) (intersection [][2]uint8) { + var j int + for _, v := range a { + for _, v2 := range b { + if v[0] == v2[0] && v[1] == v2[1] { + a[j] = v + j++ + break + } + } + } + + return a[:j] +} + +func hashToHashId(h crypto.Hash) uint8 { + v, ok := algorithm.HashToHashId(h) + if !ok { + panic("tried to convert unknown hash") + } + return v +} + +// EncryptWithParams encrypts a message to a number of recipients and, optionally, +// signs it. The resulting WriteCloser must be closed after the contents of the file have been written. +// The to argument contains recipients that are explicitly mentioned in signatures and encrypted keys, +// whereas the toHidden argument contains recipients that will be hidden and not mentioned. +// Params contains all optional parameters. +func EncryptWithParams(ciphertext io.Writer, to, toHidden []*Entity, params *EncryptParams) (plaintext io.WriteCloser, err error) { + if params == nil { + params = &EncryptParams{} + } + if params.KeyWriter == nil { + params.KeyWriter = ciphertext + } + return encrypt(to, toHidden, ciphertext, params) +} + +// Encrypt encrypts a message to a number of recipients and, optionally, signs +// it. Hints contains optional information, that is also encrypted, that aids +// the recipients in processing the message. The crypto-refresh recommends +// to not set file hints since the data is not included in the signature hash. +// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#section-5.9. +// The resulting WriteCloser must be closed after the contents of the file have been written. +// The to argument contains recipients that are explicitly mentioned in signatures and encrypted keys, +// whereas the toHidden argument contains recipients that will be hidden and not mentioned. +// If config is nil, sensible defaults will be used. +func Encrypt(ciphertext io.Writer, to, toHidden []*Entity, signers []*Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) { + return EncryptWithParams(ciphertext, to, toHidden, &EncryptParams{ + Signers: signers, + Hints: hints, + Config: config, + }) +} + +// writeAndSign writes the data as a payload package and, optionally, signs +// it. Hints contains optional information, that is also encrypted, +// that aids the recipients in processing the message. The resulting +// WriteCloser must be closed after the contents of the file have been +// written. If config is nil, sensible defaults will be used. +func writeAndSign(payload io.WriteCloser, candidateHashes [][]uint8, signEntities []*Entity, hints *FileHints, sigType packet.SignatureType, intendedRecipients []*packet.Recipient, outsideSig []byte, config *packet.Config) (plaintext io.WriteCloser, err error) { + var signers []*signatureContext + var numberOfOutsideSigs int + + if outsideSig != nil { + outSigPacket, err := parseOutsideSig(outsideSig) + if err != nil { + return nil, err + } + opsVersion := 3 + if outSigPacket.Version == 6 { + opsVersion = 6 + } + opsOutside := &packet.OnePassSignature{ + Version: opsVersion, + SigType: outSigPacket.SigType, + Hash: outSigPacket.Hash, + PubKeyAlgo: outSigPacket.PubKeyAlgo, + KeyId: *outSigPacket.IssuerKeyId, + IsLast: len(signEntities) == 0, + } + sigContext := signatureContext{ + outsideSig: outSigPacket, + } + if outSigPacket.Version == 6 { + opsOutside.KeyFingerprint = outSigPacket.IssuerFingerprint + sigContext.salt = outSigPacket.Salt() + opsOutside.Salt = outSigPacket.Salt() + } + sigContext.h, sigContext.wrappedHash, err = hashForSignature(outSigPacket.Hash, sigType, sigContext.salt) + if err != nil { + return nil, err + } + if err := opsOutside.Serialize(payload); err != nil { + return nil, err + } + signers = append([]*signatureContext{&sigContext}, signers...) + numberOfOutsideSigs = 1 + } + + for signEntityIdx, signEntity := range signEntities { + if signEntity == nil { + continue + } + signKey, ok := signEntity.SigningKeyById(config.Now(), config.SigningKey(), config) + if !ok { + return nil, errors.InvalidArgumentError("no valid signing keys") + } + signer := signKey.PrivateKey + if signer == nil { + return nil, errors.InvalidArgumentError("no private key in signing key") + } + if signer.Encrypted { + return nil, errors.InvalidArgumentError("signing key must be decrypted") + } + sigContext := signatureContext{ + signer: signer, + } + + if signKey.PrimarySelfSignature == nil { + return nil, errors.InvalidArgumentError("signing key has no self-signature") + } + candidateHashes[signEntityIdx] = intersectPreferences(candidateHashes[signEntityIdx], signKey.PrimarySelfSignature.PreferredHash) + hash, err := selectHash(candidateHashes[signEntityIdx], config.Hash(), signKey.PrivateKey) + if err != nil { + return nil, err + } + sigContext.hashType = hash + + var opsVersion = 3 + if signer.Version == 6 { + opsVersion = signer.Version + } + isLast := signEntityIdx == len(signEntities)-1 + ops := &packet.OnePassSignature{ + Version: opsVersion, + SigType: sigType, + Hash: hash, + PubKeyAlgo: signer.PubKeyAlgo, + KeyId: signer.KeyId, + IsLast: isLast, + } + if opsVersion == 6 { + ops.KeyFingerprint = signer.Fingerprint + sigContext.salt, err = packet.SignatureSaltForHash(hash, config.Random()) + if err != nil { + return nil, err + } + ops.Salt = sigContext.salt + } + if err := ops.Serialize(payload); err != nil { + return nil, err + } + + sigContext.h, sigContext.wrappedHash, err = hashForSignature(hash, sigType, sigContext.salt) + if err != nil { + return nil, err + } + // Prepend since the last signature has to be written first + signers = append([]*signatureContext{&sigContext}, signers...) + } + + if signEntities != nil && len(signEntities)+numberOfOutsideSigs != len(signers) { + return nil, errors.InvalidArgumentError("no valid signing key") + } + + if hints == nil { + hints = &FileHints{} + } + + w := payload + if signers != nil || numberOfOutsideSigs > 0 { + // If we need to write a signature packet after the literal + // data then we need to stop literalData from closing + // encryptedData. + w = noOpCloser{w} + + } + var epochSeconds uint32 + if !hints.ModTime.IsZero() { + epochSeconds = uint32(hints.ModTime.Unix()) + } + literalData, err := packet.SerializeLiteral(w, !hints.IsUTF8, hints.FileName, epochSeconds) + if err != nil { + return nil, err + } + + if signers != nil || numberOfOutsideSigs > 0 { + metadata := &packet.LiteralData{ + Format: 'b', + FileName: hints.FileName, + Time: epochSeconds, + } + if hints.IsUTF8 { + metadata.Format = 'u' + } + return signatureWriter{payload, literalData, signers, sigType, config, metadata, intendedRecipients, 0}, nil + } + return literalData, nil +} + +// encrypt encrypts a message to a number of recipients and, optionally, signs +// it. The resulting WriteCloser must +// be closed after the contents of the file have been written. +func encrypt( + to, toHidden []*Entity, + dataWriter io.Writer, + params *EncryptParams, +) (plaintext io.WriteCloser, err error) { + if len(to)+len(toHidden) == 0 { + return nil, errors.InvalidArgumentError("no encryption recipient provided") + } + + // These are the possible ciphers that we'll use for the message. + candidateCiphers := []uint8{ + uint8(packet.CipherAES256), + uint8(packet.CipherAES128), + } + + // These are the possible hash functions that we'll use for the signature. + candidateHashes := []uint8{ + hashToHashId(crypto.SHA256), + hashToHashId(crypto.SHA384), + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), + } + + // Prefer GCM if everyone supports it + candidateCipherSuites := [][2]uint8{ + {uint8(packet.CipherAES256), uint8(packet.AEADModeGCM)}, + {uint8(packet.CipherAES256), uint8(packet.AEADModeEAX)}, + {uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeGCM)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeEAX)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}, + } + + candidateCompression := []uint8{ + uint8(packet.CompressionNone), + uint8(packet.CompressionZIP), + uint8(packet.CompressionZLIB), + } + + encryptKeys := make([]Key, len(to)+len(toHidden)) + + config := params.Config + // AEAD is used only if config enables it and every key supports it + aeadSupported := config.AEAD() != nil + + var intendedRecipients []*packet.Recipient + // Intended Recipient Fingerprint subpacket SHOULD be used when creating a signed and encrypted message + for _, publicRecipient := range to { + if config.IntendedRecipients() { + intendedRecipients = append(intendedRecipients, &packet.Recipient{KeyVersion: publicRecipient.PrimaryKey.Version, Fingerprint: publicRecipient.PrimaryKey.Fingerprint}) + } + } + + timeForEncryptionKey := config.Now() + if params.EncryptionTime != nil { + // Override the time to select the encryption key with the provided one. + timeForEncryptionKey = *params.EncryptionTime + } + for i, recipient := range append(to, toHidden...) { + var ok bool + encryptKeys[i], ok = recipient.EncryptionKey(timeForEncryptionKey, config) + if !ok { + return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") + } + + primarySelfSignature, _ := recipient.PrimarySelfSignature(timeForEncryptionKey) + if primarySelfSignature == nil { + return nil, errors.StructuralError("entity without a self-signature") + } + + if !primarySelfSignature.SEIPDv2 { + aeadSupported = false + } + + candidateCiphers = intersectPreferences(candidateCiphers, primarySelfSignature.PreferredSymmetric) + candidateHashes = intersectPreferences(candidateHashes, primarySelfSignature.PreferredHash) + candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, primarySelfSignature.PreferredCipherSuites) + candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression) + } + + // In the event that the intersection of supported algorithms is empty we use the ones + // labelled as MUST that every implementation supports. + if len(candidateCiphers) == 0 { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.3 + candidateCiphers = []uint8{uint8(packet.CipherAES128)} + } + if len(candidateHashes) == 0 { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#hash-algos + candidateHashes = []uint8{hashToHashId(crypto.SHA256)} + } + if len(candidateCipherSuites) == 0 { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } + + cipher := packet.CipherFunction(candidateCiphers[0]) + aeadCipherSuite := packet.CipherSuite{ + Cipher: packet.CipherFunction(candidateCipherSuites[0][0]), + Mode: packet.AEADMode(candidateCipherSuites[0][1]), + } + + // If the cipher specified by config is a candidate, we'll use that. + configuredCipher := config.Cipher() + for _, c := range candidateCiphers { + cipherFunc := packet.CipherFunction(c) + if cipherFunc == configuredCipher { + cipher = cipherFunc + break + } + } + + if params.SessionKey == nil { + params.SessionKey = make([]byte, cipher.KeySize()) + if _, err := io.ReadFull(config.Random(), params.SessionKey); err != nil { + return nil, err + } + defer func() { + // zero the session key after we are done + for i := range params.SessionKey { + params.SessionKey[i] = 0 + } + params.SessionKey = nil + }() + } + + for idx, key := range encryptKeys { + // hide the keys of the hidden recipients + hidden := idx >= len(to) + if err := packet.SerializeEncryptedKeyAEADwithHiddenOption(params.KeyWriter, key.PublicKey, cipher, aeadSupported, params.SessionKey, hidden, config); err != nil { + return nil, err + } + } + + for _, password := range params.Passwords { + if err = packet.SerializeSymmetricKeyEncryptedReuseKey(params.KeyWriter, params.SessionKey, password, params.Config); err != nil { + return nil, err + } + } + + var candidateHashesPerSignature [][]uint8 + for range params.Signers { + candidateHashesPerSignature = append(candidateHashesPerSignature, candidateHashes) + } + return encryptDataAndSign(dataWriter, params, candidateHashesPerSignature, candidateCompression, cipher, aeadSupported, aeadCipherSuite, intendedRecipients) +} + +func encryptDataAndSign( + dataWriter io.Writer, + params *EncryptParams, + candidateHashes [][]uint8, + candidateCompression []uint8, + cipher packet.CipherFunction, + aeadSupported bool, + aeadCipherSuite packet.CipherSuite, + intendedRecipients []*packet.Recipient, +) (plaintext io.WriteCloser, err error) { + sigType := packet.SigTypeBinary + if params.TextSig { + sigType = packet.SigTypeText + } + payload, err := packet.SerializeSymmetricallyEncrypted(dataWriter, cipher, aeadSupported, aeadCipherSuite, params.SessionKey, params.Config) + if err != nil { + return + } + payload, err = handleCompression(payload, candidateCompression, params.Config) + if err != nil { + return nil, err + } + return writeAndSign(payload, candidateHashes, params.Signers, params.Hints, sigType, intendedRecipients, params.OutsideSig, params.Config) +} + +type SignParams struct { + // Hints contains file metadata for the literal data packet. + // The crypto-refresh recommends to not set file hints since the data is not included in the signature hash. + // See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#section-5.9. + // If nil, default is used. + Hints *FileHints + // TextSig indicates if signatures of type SigTypeText should be produced + TextSig bool + // OutsideSig allows to set a signature that should be included + // in an inline signed message. + // Should only be used for exceptional cases. + // If nil, ignored. + OutsideSig []byte + // Config provides the config to be used. + // If Config is nil, sensible defaults will be used. + Config *packet.Config +} + +// SignWithParams signs a message. The resulting WriteCloser must be closed after the +// contents of the file have been written. +// SignParams can contain optional params and can be nil for defaults. +func SignWithParams(output io.Writer, signers []*Entity, params *SignParams) (input io.WriteCloser, err error) { + if params == nil { + params = &SignParams{} + } + if len(signers) < 1 && params.OutsideSig == nil { + return nil, errors.InvalidArgumentError("no signer provided") + } + var candidateHashesPerSignature [][]uint8 + candidateCompression := []uint8{ + uint8(packet.CompressionNone), + uint8(packet.CompressionZIP), + uint8(packet.CompressionZLIB), + } + for _, signer := range signers { + // These are the possible hash functions that we'll use for the signature. + candidateHashes := []uint8{ + hashToHashId(crypto.SHA256), + hashToHashId(crypto.SHA384), + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), + } + defaultHashes := candidateHashes[0:1] + primarySelfSignature, _ := signer.PrimarySelfSignature(params.Config.Now()) + if primarySelfSignature == nil { + return nil, errors.StructuralError("signed entity has no self-signature") + } + preferredHashes := primarySelfSignature.PreferredHash + if len(preferredHashes) == 0 { + preferredHashes = defaultHashes + } + candidateHashes = intersectPreferences(candidateHashes, preferredHashes) + if len(candidateHashes) == 0 { + return nil, errors.StructuralError("cannot sign because signing key shares no common algorithms with candidate hashes") + } + candidateHashesPerSignature = append(candidateHashesPerSignature, candidateHashes) + candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression) + + } + + sigType := packet.SigTypeBinary + if params.TextSig { + sigType = packet.SigTypeText + } + + var payload io.WriteCloser + payload = noOpCloser{output} + payload, err = handleCompression(payload, candidateCompression, params.Config) + if err != nil { + return nil, err + } + return writeAndSign(payload, candidateHashesPerSignature, signers, params.Hints, sigType, nil, params.OutsideSig, params.Config) +} + +// Sign signs a message. The resulting WriteCloser must be closed after the +// contents of the file have been written. Hints contains optional information +// that aids the recipients in processing the message. +// The crypto-refresh recommends to not set file hints since the data is not included in the signature hash. +// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#section-5.9. +// If config is nil, sensible defaults will be used. +func Sign(output io.Writer, signers []*Entity, hints *FileHints, config *packet.Config) (input io.WriteCloser, err error) { + return SignWithParams(output, signers, &SignParams{ + Config: config, + Hints: hints, + }) +} + +// signatureWriter hashes the contents of a message while passing it along to +// literalData. When closed, it closes literalData, writes a signature packet +// to encryptedData and then also closes encryptedData. +type signatureWriter struct { + encryptedData io.WriteCloser + literalData io.WriteCloser + signatureContexts []*signatureContext + sigType packet.SignatureType + config *packet.Config + metadata *packet.LiteralData // V5 signatures protect document metadata + intendedRecipients []*packet.Recipient + flag int +} + +type signatureContext struct { + hashType crypto.Hash + wrappedHash hash.Hash + h hash.Hash + salt []byte // v6 only + signer *packet.PrivateKey + outsideSig *packet.Signature +} + +func (s signatureWriter) Write(data []byte) (int, error) { + for _, ctx := range s.signatureContexts { + if _, err := ctx.wrappedHash.Write(data); err != nil { + return 0, err + } + } + switch s.sigType { + case packet.SigTypeBinary: + return s.literalData.Write(data) + case packet.SigTypeText: + return writeCanonical(s.literalData, data, &s.flag) + } + return 0, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(s.sigType))) +} + +func (s signatureWriter) Close() error { + if err := s.literalData.Close(); err != nil { + return err + } + for _, ctx := range s.signatureContexts { + var sig *packet.Signature + if ctx.outsideSig != nil { + // Signature that was supplied outside + sig = ctx.outsideSig + } else { + sig = createSignaturePacket(&ctx.signer.PublicKey, s.sigType, s.config) + sig.Hash = ctx.hashType + sig.Metadata = s.metadata + sig.IntendedRecipients = s.intendedRecipients + if err := sig.SetSalt(ctx.salt); err != nil { + return err + } + if err := sig.Sign(ctx.h, ctx.signer, s.config); err != nil { + return err + } + } + if err := sig.Serialize(s.encryptedData); err != nil { + return err + } + } + return s.encryptedData.Close() +} + +func adaptHashToSigningKey(config *packet.Config, primary *packet.PublicKey) crypto.Hash { + acceptableHashes := acceptableHashesToWrite(primary) + hash, ok := algorithm.HashToHashId(config.Hash()) + if !ok { + return config.Hash() + } + for _, acceptableHashes := range acceptableHashes { + if acceptableHashes == hash { + return config.Hash() + } + } + if len(acceptableHashes) > 0 { + defaultAcceptedHash, ok := algorithm.HashIdToHash(acceptableHashes[0]) + if !ok { + return config.Hash() + } + return defaultAcceptedHash + } + return config.Hash() +} + +func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature { + sigLifetimeSecs := config.SigLifetime() + hash := adaptHashToSigningKey(config, signer) + return &packet.Signature{ + Version: signer.Version, + SigType: sigType, + PubKeyAlgo: signer.PubKeyAlgo, + Hash: hash, + CreationTime: config.Now(), + IssuerKeyId: &signer.KeyId, + IssuerFingerprint: signer.Fingerprint, + Notations: config.Notations(), + SigLifetimeSecs: &sigLifetimeSecs, + } +} + +// noOpCloser is like an ioutil.NopCloser, but for an io.Writer. +// TODO: we have two of these in OpenPGP packages alone. This probably needs +// to be promoted somewhere more common. +type noOpCloser struct { + w io.Writer +} + +func (c noOpCloser) Write(data []byte) (n int, err error) { + return c.w.Write(data) +} + +func (c noOpCloser) Close() error { + return nil +} + +func handleCompression(compressed io.WriteCloser, candidateCompression []uint8, config *packet.Config) (data io.WriteCloser, err error) { + data = compressed + confAlgo := config.Compression() + if confAlgo == packet.CompressionNone { + return + } + + // Set algorithm labelled as MUST as fallback + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.4 + finalAlgo := packet.CompressionNone + // if compression specified by config available we will use it + for _, c := range candidateCompression { + if uint8(confAlgo) == c { + finalAlgo = confAlgo + break + } + } + + if finalAlgo != packet.CompressionNone { + var compConfig *packet.CompressionConfig + if config != nil { + compConfig = config.CompressionConfig + } + data, err = packet.SerializeCompressed(compressed, finalAlgo, compConfig) + if err != nil { + return + } + } + return data, nil +} + +// selectHash selects the preferred hash given the candidateHashes and the configuredHash +func selectHash(candidateHashes []byte, configuredHash crypto.Hash, signer *packet.PrivateKey) (hash crypto.Hash, err error) { + acceptableHashes := acceptableHashesToWrite(&signer.PublicKey) + candidateHashes = intersectPreferences(acceptableHashes, candidateHashes) + + for _, hashId := range candidateHashes { + if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() { + hash = h + break + } + } + + // If the hash specified by config is a candidate, we'll use that. + if configuredHash.Available() { + for _, hashId := range candidateHashes { + if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash { + hash = h + break + } + } + } + + if hash == 0 { + if len(acceptableHashes) > 0 { + if h, ok := algorithm.HashIdToHash(acceptableHashes[0]); ok { + hash = h + } else { + return 0, errors.UnsupportedError("no candidate hash functions are compiled in.") + } + } else { + return 0, errors.UnsupportedError("no candidate hash functions are compiled in.") + } + } + return +} + +func parseOutsideSig(outsideSig []byte) (outSigPacket *packet.Signature, err error) { + packets := packet.NewReader(bytes.NewReader(outsideSig)) + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + return nil, errors.ErrUnknownIssuer + } + if err != nil { + return nil, err + } + + var ok bool + outSigPacket, ok = p.(*packet.Signature) + if !ok { + return nil, errors.StructuralError("non signature packet found") + } + if outSigPacket.IssuerKeyId == nil { + return nil, errors.StructuralError("signature doesn't have an issuer") + } + return outSigPacket, nil +} + +func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { + switch singingKey.PubKeyAlgo { + case packet.PubKeyAlgoEd448: + return []uint8{ + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_512), + } + case packet.PubKeyAlgoECDSA, packet.PubKeyAlgoEdDSA: + if curve, err := singingKey.Curve(); err == nil { + if curve == packet.Curve448 || + curve == packet.CurveNistP521 || + curve == packet.CurveBrainpoolP512 { + return []uint8{ + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_512), + } + } else if curve == packet.CurveBrainpoolP384 || + curve == packet.CurveNistP384 { + return []uint8{ + hashToHashId(crypto.SHA384), + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_512), + } + } + } + } + return []uint8{ + hashToHashId(crypto.SHA256), + hashToHashId(crypto.SHA384), + hashToHashId(crypto.SHA512), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), + } +} diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go new file mode 100644 index 000000000..0b61578b4 --- /dev/null +++ b/openpgp/v2/write_test.go @@ -0,0 +1,994 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + "bytes" + "crypto/rand" + "io" + mathrand "math/rand" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" +) + +const ( + maxPlaintextLen = 1 << 12 + maxPassLen = 1 << 6 +) + +func TestSignDetached(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + out := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + err := DetachSign(out, kring[:1], message, &allowAllAlgorithmsConfig) + if err != nil { + t.Error(err) + } + + testDetachedSignature(t, kring, out, signedInput, "check", testKey1KeyId) +} + +func TestSignTextDetached(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + out := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + err := DetachSignWithParams(out, kring[:1], message, &SignParams{ + TextSig: true, + Config: &allowAllAlgorithmsConfig, + }) + if err != nil { + t.Error(err) + } + + testDetachedSignature(t, kring, out, signedInput, "check", testKey1KeyId) +} + +func TestSignDetachedDSA(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(dsaTestKeyPrivateHex)) + out := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + err := DetachSign(out, kring[:1], message, &allowAllAlgorithmsConfig) + if err != nil { + t.Error(err) + } + + testDetachedSignature(t, kring, out, signedInput, "check", testKey3KeyId) +} + +func TestSignDetachedP256(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(p256TestKeyPrivateHex)) + if err := kring[0].PrivateKey.Decrypt([]byte("passphrase")); err != nil { + t.Error(err) + } + + out := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + err := DetachSign(out, kring[:1], message, nil) + if err != nil { + t.Error(err) + } + + testDetachedSignature(t, kring, out, signedInput, "check", testKeyP256KeyId) +} + +func TestSignDetachedWithNotation(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + signature := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + config := allowAllAlgorithmsConfig + config.SignatureNotations = []*packet.Notation{ + { + Name: "test@example.com", + Value: []byte("test"), + IsHumanReadable: true, + }, + } + err := DetachSign(signature, kring[:1], message, &config) + if err != nil { + t.Error(err) + } + + signed := bytes.NewBufferString(signedInput) + sig, signer, err := VerifyDetachedSignature(kring, signed, signature, &allowAllAlgorithmsConfig) + if err != nil { + t.Errorf("signature error: %s", err) + return + } + if sig == nil { + t.Errorf("sig is nil") + return + } + if numNotations, numExpected := len(sig.Notations), 1; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + if sig.Notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + if sig.Notations[0].Name != "test@example.com" { + t.Fatalf("got %s, expected test@example.com", sig.Notations[0].Name) + } + if string(sig.Notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(sig.Notations[0].Value)) + } + if signer == nil { + t.Errorf("signer is nil") + return + } + if signer.PrimaryKey.KeyId != testKey1KeyId { + t.Errorf("wrong signer: got %x, expected %x", signer.PrimaryKey.KeyId, testKey1KeyId) + } +} + +func TestSignDetachedWithCriticalNotation(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + signature := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + config := allowAllAlgorithmsConfig + config.SignatureNotations = []*packet.Notation{ + { + Name: "test@example.com", + Value: []byte("test"), + IsHumanReadable: true, + IsCritical: true, + }, + } + err := DetachSign(signature, kring[:1], message, &config) + if err != nil { + t.Error(err) + } + + signed := bytes.NewBufferString(signedInput) + config = allowAllAlgorithmsConfig + config.KnownNotations = map[string]bool{ + "test@example.com": true, + } + sig, signer, err := VerifyDetachedSignature(kring, signed, signature, &config) + if err != nil { + t.Errorf("signature error: %s", err) + return + } + if sig == nil { + t.Errorf("sig is nil") + return + } + if numNotations, numExpected := len(sig.Notations), 1; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + if sig.Notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + if sig.Notations[0].Name != "test@example.com" { + t.Fatalf("got %s, expected test@example.com", sig.Notations[0].Name) + } + if string(sig.Notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(sig.Notations[0].Value)) + } + if signer == nil { + t.Errorf("signer is nil") + return + } + if signer.PrimaryKey.KeyId != testKey1KeyId { + t.Errorf("wrong signer: got %x, expected %x", signer.PrimaryKey.KeyId, testKey1KeyId) + } +} + +func TestNewEntity(t *testing.T) { + // Check bit-length with no config. + e, err := NewEntity("Test User", "test", "test@example.com", nil) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + bl, err := e.PrimaryKey.BitLength() + if err != nil { + t.Errorf("failed to find bit length: %s", err) + } + defaultRSAKeyBits := 2048 + if int(bl) != defaultRSAKeyBits { + t.Errorf("BitLength %v, expected %v", int(bl), defaultRSAKeyBits) + } + + // Check bit-length with a config. + cfg := &packet.Config{RSABits: 1024} + e, err = NewEntity("Test User", "test", "test@example.com", cfg) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + bl, err = e.PrimaryKey.BitLength() + if err != nil { + t.Errorf("failed to find bit length: %s", err) + } + if int(bl) != cfg.RSABits { + t.Errorf("BitLength %v, expected %v", bl, cfg.RSABits) + } + + w := bytes.NewBuffer(nil) + if err := e.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + return + } + serialized := w.Bytes() + + el, err := ReadKeyRing(w) + if err != nil { + t.Errorf("failed to reparse entity: %s", err) + return + } + + if len(el) != 1 { + t.Errorf("wrong number of entities found, got %d, want 1", len(el)) + } + + w = bytes.NewBuffer(nil) + if err := e.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity second time: %s", err) + return + } + + if !bytes.Equal(w.Bytes(), serialized) { + t.Errorf("results differed") + } + + if err := e.PrivateKey.Encrypt([]byte("password")); err != nil { + t.Errorf("failed to encrypt private key: %s", err) + } + + if err := e.PrivateKey.Decrypt([]byte("password")); err != nil { + t.Errorf("failed to decrypt private key: %s", err) + } + + w = bytes.NewBuffer(nil) + if err := e.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize after encryption round: %s", err) + return + } + + _, err = ReadKeyRing(w) + if err != nil { + t.Errorf("failed to reparse entity after encryption round: %s", err) + return + } +} + +func TestEncryptWithCompression(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Errorf("failed to decrypt key: %s", err) + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Errorf("failed to decrypt subkey: %s", err) + } + } + } + } + + buf := new(bytes.Buffer) + config := allowAllAlgorithmsConfig + config.DefaultCompressionAlgo = packet.CompressionZIP + config.CompressionConfig = &packet.CompressionConfig{Level: -1} + w, err := Encrypt(buf, kring[:1], nil, nil, nil /* no hints */, &config) + if err != nil { + t.Errorf("error in encrypting plaintext: %s", err) + return + } + message := []byte("hello world") + _, err = w.Write(message) + + if err != nil { + t.Errorf("error writing plaintext: %s", err) + return + } + err = w.Close() + if err != nil { + t.Errorf("error closing WriteCloser: %s", err) + return + } + err = checkCompression(buf, kring[:1]) + if err != nil { + t.Errorf("compression check failed: %s", err) + } +} + +func TestSymmetricEncryption(t *testing.T) { + modesS2K := map[string]s2k.Mode{ + "Iterated": s2k.IteratedSaltedS2K, + "Argon2": s2k.Argon2S2K, + } + for s2kName, s2ktype := range modesS2K { + t.Run(s2kName, func(t *testing.T) { + config := &packet.Config{ + S2KConfig: &s2k.Config{S2KMode: s2ktype}, + } + buf := new(bytes.Buffer) + plaintext, err := SymmetricallyEncrypt(buf, []byte("testing"), nil, config) + if err != nil { + t.Errorf("error writing headers: %s", err) + return + } + message := []byte("hello world\n") + _, err = plaintext.Write(message) + if err != nil { + t.Errorf("error writing to plaintext writer: %s", err) + } + err = plaintext.Close() + if err != nil { + t.Errorf("error closing plaintext writer: %s", err) + } + + md, err := ReadMessage(buf, nil, func(keys []Key, symmetric bool) ([]byte, error) { + return []byte("testing"), nil + }, nil) + if err != nil { + t.Errorf("error rereading message: %s", err) + } + messageBuf := bytes.NewBuffer(nil) + _, err = io.Copy(messageBuf, md.UnverifiedBody) + if err != nil { + t.Errorf("error rereading message: %s", err) + } + if !bytes.Equal(message, messageBuf.Bytes()) { + t.Errorf("recovered message incorrect got '%s', want '%s'", messageBuf.Bytes(), message) + } + }) + } +} + +func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { + modesS2K := map[int]s2k.Mode{ + 0: s2k.IteratedSaltedS2K, + 1: s2k.Argon2S2K, + } + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config := &packet.Config{AEADConfig: &aeadConf, S2KConfig: &s2k.Config{S2KMode: modesS2K[mathrand.Intn(2)]}} + buf := new(bytes.Buffer) + passphrase := make([]byte, mathrand.Intn(maxPassLen)) + _, err := rand.Read(passphrase) + if err != nil { + panic(err) + } + plaintext, err := SymmetricallyEncrypt(buf, passphrase, nil, config) + if err != nil { + t.Errorf("error writing headers: %s", err) + return + } + message := make([]byte, mathrand.Intn(maxPlaintextLen)) + _, errR := rand.Read(message) + if errR != nil { + panic(errR) + } + _, err = plaintext.Write(message) + if err != nil { + t.Errorf("error writing to plaintext writer: %s", err) + } + err = plaintext.Close() + if err != nil { + t.Errorf("error closing plaintext writer: %s", err) + } + + // Check if the packet is AEADEncrypted + copiedCiph := make([]byte, len(buf.Bytes())) + copy(copiedCiph, buf.Bytes()) + copiedBuf := bytes.NewBuffer(copiedCiph) + packets := packet.NewReader(copiedBuf) + // First a SymmetricKeyEncrypted packet + p, err := packets.Next() + if err != nil { + t.Errorf("error reading packet: %s", err) + } + switch tp := p.(type) { + case *packet.SymmetricKeyEncrypted: + default: + t.Errorf("Didn't find a SymmetricKeyEncrypted packet (found %T instead)", tp) + } + // Then an SymmetricallyEncrypted packet version 2 + p, err = packets.Next() + if err != nil { + t.Errorf("error reading packet: %s", err) + } + switch tp := p.(type) { + case *packet.SymmetricallyEncrypted: + if tp.Version != 2 { + t.Errorf("Wrong packet version, expected 2, found %d", tp.Version) + } + default: + t.Errorf("Didn't find an SymmetricallyEncrypted packet (found %T instead)", tp) + } + + promptFunc := func(keys []Key, symmetric bool) ([]byte, error) { + return passphrase, nil + } + md, err := ReadMessage(buf, nil, promptFunc, config) + if err != nil { + t.Errorf("error rereading message: %s", err) + } + messageBuf := bytes.NewBuffer(nil) + _, err = io.Copy(messageBuf, md.UnverifiedBody) + if err != nil { + t.Errorf("error rereading message: %s", err) + } + if !bytes.Equal(message, messageBuf.Bytes()) { + t.Errorf("recovered message incorrect got '%s', want '%s'", + messageBuf.Bytes(), message) + } +} + +var testEncryptionTests = []struct { + keyRingHex string + isSigned bool + okV6 bool +}{ + { + testKeys1And2PrivateHex, + false, + true, + }, + { + testKeys1And2PrivateHex, + true, + true, + }, + { + dsaElGamalTestKeysHex, + false, + false, + }, + { + dsaElGamalTestKeysHex, + true, + false, + }, +} + +func TestIntendedRecipientsEncryption(t *testing.T) { + var config = &packet.Config{ + V6Keys: true, + AEADConfig: &packet.AEADConfig{}, + Algorithm: packet.PubKeyAlgoEd25519, + } + sender, err := NewEntity("sender", "", "send@example.com", config) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + + publicRecipient, err := NewEntity("publicRecipient", "", "publicRecipient@example.com", config) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + + hiddenRecipient, err := NewEntity("hiddenRecipient", "", "hiddenRecipient@example.com", config) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + + outputBuffer := new(bytes.Buffer) + pWriter, err := Encrypt(outputBuffer, []*Entity{publicRecipient}, []*Entity{hiddenRecipient}, []*Entity{sender}, nil, config) + if err != nil { + t.Errorf("error in encrypt: %s", err) + } + + const message = "testing" + _, err = pWriter.Write([]byte(message)) + if err != nil { + t.Errorf("error writing plaintext: %s", err) + } + + err = pWriter.Close() + if err != nil { + t.Errorf("error closing WriteCloser: %s", err) + } + + encryptedMessage := make([]byte, len(outputBuffer.Bytes())) + copy(encryptedMessage, outputBuffer.Bytes()) + + md, err := ReadMessage(outputBuffer, EntityList{publicRecipient, sender}, nil /* no prompt */, config) + if err != nil { + t.Errorf("error reading message: %s", err) + } + + // Check reading with public recipient + if !md.CheckRecipients { + t.Error("should check for intended recipient") + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading encrypted contents: %s", err) + } + + if md.Signature == nil { + t.Error("expected matching signature") + } + + if len(md.Signature.IntendedRecipients) == 0 || + !bytes.Equal(md.Signature.IntendedRecipients[0].Fingerprint, publicRecipient.PrimaryKey.Fingerprint) { + t.Errorf("signature should contain %s as recipient", publicRecipient.PrimaryKey.Fingerprint) + } + + // Check reading with hidden recipient + outputBuffer = new(bytes.Buffer) + outputBuffer.Write(encryptedMessage) + md, err = ReadMessage(outputBuffer, EntityList{hiddenRecipient, sender}, nil /* no prompt */, config) + if err != nil { + t.Errorf("error reading message: %s", err) + } + if !md.CheckRecipients { + t.Error("should check for intended recipient") + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading encrypted contents: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if _, ok := md.SignatureError.(errors.SignatureError); !ok { + t.Error("hidden recipient should not be in the intended recipient list") + } + + // Check reading with hidden recipient check disabled + outputBuffer = new(bytes.Buffer) + outputBuffer.Write(encryptedMessage) + check := false + config.CheckIntendedRecipients = &check + md, err = ReadMessage(outputBuffer, EntityList{hiddenRecipient, sender}, nil /* no prompt */, config) + if err != nil { + t.Errorf("error reading message: %s", err) + } + if md.CheckRecipients { + t.Error("should not check for intended recipient") + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading encrypted contents: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Error("signature verification should pass") + } +} + +func TestMultiSignEncryption(t *testing.T) { + recipient, err := NewEntity("sender", "", "send@example.com", nil) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + + v4Sign, err := NewEntity("signv4", "", "signv4@example.com", &packet.Config{ + Algorithm: packet.PubKeyAlgoRSA, + }) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + + v6Sign, err := NewEntity("signv6", "", "signv6@example.com", &packet.Config{ + V6Keys: true, + Algorithm: packet.PubKeyAlgoEd25519, + }) + if err != nil { + t.Errorf("failed to create entity: %s", err) + return + } + + outputBuffer := new(bytes.Buffer) + pWriter, err := Encrypt(outputBuffer, []*Entity{recipient}, nil, []*Entity{v4Sign, v6Sign}, nil, nil) + if err != nil { + t.Errorf("error in encrypt: %s", err) + } + + const message = "testing" + _, err = pWriter.Write([]byte(message)) + if err != nil { + t.Errorf("error writing plaintext: %s", err) + } + + err = pWriter.Close() + if err != nil { + t.Errorf("error closing WriteCloser: %s", err) + } + + encryptedMessage := make([]byte, len(outputBuffer.Bytes())) + copy(encryptedMessage, outputBuffer.Bytes()) + + md, err := ReadMessage(outputBuffer, EntityList{recipient, v4Sign}, nil /* no prompt */, nil) + if err != nil { + t.Errorf("error reading message: %s", err) + } + + // Check reading with v4 key + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading encrypted contents: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.Signature == nil || md.SignatureError != nil { + t.Error("expected matching signature") + } + + // Check reading with v6 key + outputBuffer = new(bytes.Buffer) + outputBuffer.Write(encryptedMessage) + md, err = ReadMessage(outputBuffer, EntityList{recipient, v6Sign}, nil /* no prompt */, nil) + if err != nil { + t.Errorf("error reading message: %s", err) + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading encrypted contents: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.Signature == nil || md.SignatureError != nil { + t.Error("expected matching signature") + } + + // Check reading with error + outputBuffer = new(bytes.Buffer) + outputBuffer.Write(encryptedMessage) + md, err = ReadMessage(outputBuffer, EntityList{recipient}, nil /* no prompt */, nil) + if err != nil { + t.Errorf("error reading message: %s", err) + } + _, err = io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("error reading encrypted contents: %s", err) + } + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.Signature != nil || md.SignatureError == nil { + t.Error("expected error") + } + if md.SignatureError != errors.ErrUnknownIssuer { + t.Error("expected unknown issuer error") + } +} + +func TestEncryption(t *testing.T) { + for i, test := range testEncryptionTests { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Errorf("#%d: failed to decrypt key", i) + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Errorf("#%d: failed to decrypt subkey", i) + } + } + } + } + + var signed *Entity + if test.isSigned { + signed = kring[0] + } + + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + config := allowAllAlgorithmsConfig + config.DefaultCompressionAlgo = compAlgo + config.CompressionConfig = compConf + + // Flip coin to enable AEAD mode + if mathrand.Int()%2 == 0 { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } + var signers []*Entity + if signed != nil { + signers = []*Entity{signed} + } + w, err := Encrypt(buf, kring[:1], nil, signers, nil /* no hints */, &config) + if (err != nil) == (test.okV6 && config.AEAD() != nil) { + // ElGamal is not allowed with v6 + continue + } + + if err != nil { + t.Errorf("#%d: error in Encrypt: %s", i, err) + continue + } + + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Errorf("#%d: error writing plaintext: %s", i, err) + continue + } + err = w.Close() + if err != nil { + t.Errorf("#%d: error closing WriteCloser: %s", i, err) + continue + } + + md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) + if err != nil { + t.Errorf("#%d: error reading message: %s", i, err) + continue + } + + testTime, _ := time.Parse("2006-01-02", "2013-07-01") + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := signKey.PublicKey.KeyId + if len(md.SignatureCandidates) < 1 { + t.Error("no candidate signature found") + } + if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { + t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + } + if md.SignatureCandidates[0].SignedByEntity == nil { + t.Errorf("#%d: failed to find the signing Entity", i) + } + } + + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("#%d: error reading encrypted contents: %s", i, err) + continue + } + + encryptKey, _ := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) + } + + if string(plaintext) != message { + t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) + } + + if test.isSigned { + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Errorf("#%d: signature error: %s", i, md.SignatureError) + } + if md.Signature == nil { + t.Error("signature missing") + } + } + } +} + +var testSigningTests = []struct { + keyRingHex string +}{ + { + testKeys1And2PrivateHex, + }, + { + dsaElGamalTestKeysHex, + }, + { + ed25519wX25519Key, + }, +} + +func TestSigning(t *testing.T) { + for i, test := range testSigningTests { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Errorf("#%d: failed to decrypt key", i) + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Errorf("#%d: failed to decrypt subkey", i) + } + } + } + } + + signed := kring[0] + + buf := new(bytes.Buffer) + w, err := Sign(buf, []*Entity{signed}, nil /* no hints */, &allowAllAlgorithmsConfig) + if err != nil { + t.Errorf("#%d: error in Sign: %s", i, err) + continue + } + + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Errorf("#%d: error writing plaintext: %s", i, err) + continue + } + err = w.Close() + if err != nil { + t.Errorf("#%d: error closing WriteCloser: %s", i, err) + continue + } + + md, err := ReadMessage(buf, kring, nil /* no prompt */, &allowAllAlgorithmsConfig) + if err != nil { + t.Errorf("#%d: error reading message: %s", i, err) + continue + } + + testTime, _ := time.Parse("2006-01-02", "2022-12-01") + signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := signKey.PublicKey.KeyId + if len(md.SignatureCandidates) < 1 { + t.Error("expected a signature candidate") + } + if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { + t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + } + if md.SignatureCandidates[0].SignedByEntity == nil { + t.Errorf("#%d: failed to find the signing Entity", i) + } + + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Errorf("#%d: error reading contents: %v", i, err) + continue + } + + if string(plaintext) != message { + t.Errorf("#%d: got: %q, want: %q", i, plaintext, message) + } + + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Errorf("#%d: signature error: %q", i, md.SignatureError) + } + if md.Signature == nil { + t.Error("signature missing") + } + } +} + +func checkCompression(r io.Reader, keyring KeyRing) (err error) { + var p packet.Packet + + var symKeys []*packet.SymmetricKeyEncrypted + var pubKeys []keyEnvelopePair + // Integrity protected encrypted packet: SymmetricallyEncrypted or AEADEncrypted + var edp packet.EncryptedDataPacket + + packets := packet.NewReader(r) + config := &packet.Config{} + + // The message, if encrypted, starts with a number of packets + // containing an encrypted decryption key. The decryption key is either + // encrypted to a public key, or with a passphrase. This loop + // collects these packets. +ParsePackets: + for { + p, err = packets.Next() + if err != nil { + return err + } + switch p := p.(type) { + case *packet.EncryptedKey: + // This packet contains the decryption key encrypted to a public key. + switch p.Algo { + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH: + break + default: + continue + } + keys := keyring.KeysById(p.KeyId) + for _, k := range keys { + pubKeys = append(pubKeys, keyEnvelopePair{k, p}) + } + case *packet.SymmetricallyEncrypted, *packet.AEADEncrypted: + edp = p.(packet.EncryptedDataPacket) + break ParsePackets + case *packet.Compressed, *packet.LiteralData, *packet.OnePassSignature: + // This message isn't encrypted. + return errors.StructuralError("message not encrypted") + } + } + + var candidates []Key + var decrypted io.ReadCloser + + // Now that we have the list of encrypted keys we need to decrypt at + // least one of them or, if we cannot, we need to call the prompt + // function so that it can decrypt a key or give us a passphrase. +FindKey: + for { + // See if any of the keys already have a private key available + candidates = candidates[:0] + candidateFingerprints := make(map[string]bool) + + for _, pk := range pubKeys { + if pk.key.PrivateKey == nil { + continue + } + if !pk.key.PrivateKey.Encrypted { + if len(pk.encryptedKey.Key) == 0 { + errDec := pk.encryptedKey.Decrypt(pk.key.PrivateKey, config) + if errDec != nil { + continue + } + } + // Try to decrypt symmetrically encrypted + decrypted, err = edp.Decrypt(pk.encryptedKey.CipherFunc, pk.encryptedKey.Key) + if err != nil && err != errors.ErrKeyIncorrect { + return err + } + if decrypted != nil { + break FindKey + } + } else { + fpr := string(pk.key.PublicKey.Fingerprint[:]) + if v := candidateFingerprints[fpr]; v { + continue + } + candidates = append(candidates, pk.key) + candidateFingerprints[fpr] = true + } + } + + if len(candidates) == 0 && len(symKeys) == 0 { + return errors.ErrKeyIncorrect + } + } + + decPackets, err := packet.Read(decrypted) + if err != nil { + return + } + _, ok := decPackets.(*packet.Compressed) + if !ok { + return errors.StructuralError("No compressed packets found") + } + return nil +} diff --git a/openpgp/write.go b/openpgp/write.go index 7fdd13a3d..0db5526ce 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -76,7 +76,11 @@ func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.S sig := createSignaturePacket(signingKey.PublicKey, sigType, config) - h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType) + h, err := sig.PrepareSign(config) + if err != nil { + return + } + wrappedHash, err := wrapHashForSignature(h, sig.SigType) if err != nil { return } @@ -275,14 +279,28 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)") } + var salt []byte if signer != nil { + var opsVersion = 3 + if signer.Version == 6 { + opsVersion = signer.Version + } ops := &packet.OnePassSignature{ + Version: opsVersion, SigType: sigType, Hash: hash, PubKeyAlgo: signer.PubKeyAlgo, KeyId: signer.KeyId, IsLast: true, } + if opsVersion == 6 { + ops.KeyFingerprint = signer.Fingerprint + salt, err = packet.SignatureSaltForHash(hash, config.Random()) + if err != nil { + return nil, err + } + ops.Salt = salt + } if err := ops.Serialize(payload); err != nil { return nil, err } @@ -310,19 +328,19 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit } if signer != nil { - h, wrappedHash, err := hashForSignature(hash, sigType) + h, wrappedHash, err := hashForSignature(hash, sigType, salt) if err != nil { return nil, err } metadata := &packet.LiteralData{ - Format: 't', + Format: 'u', FileName: hints.FileName, Time: epochSeconds, } if hints.IsBinary { metadata.Format = 'b' } - return signatureWriter{payload, literalData, hash, wrappedHash, h, signer, sigType, config, metadata}, nil + return signatureWriter{payload, literalData, hash, wrappedHash, h, salt, signer, sigType, config, metadata}, nil } return literalData, nil } @@ -380,15 +398,19 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") } - sig := to[i].PrimaryIdentity().SelfSignature - if !sig.SEIPDv2 { + primarySelfSignature, _ := to[i].PrimarySelfSignature() + if primarySelfSignature == nil { + return nil, errors.InvalidArgumentError("entity without a self-signature") + } + + if !primarySelfSignature.SEIPDv2 { aeadSupported = false } - candidateCiphers = intersectPreferences(candidateCiphers, sig.PreferredSymmetric) - candidateHashes = intersectPreferences(candidateHashes, sig.PreferredHash) - candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, sig.PreferredCipherSuites) - candidateCompression = intersectPreferences(candidateCompression, sig.PreferredCompression) + candidateCiphers = intersectPreferences(candidateCiphers, primarySelfSignature.PreferredSymmetric) + candidateHashes = intersectPreferences(candidateHashes, primarySelfSignature.PreferredHash) + candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, primarySelfSignature.PreferredCipherSuites) + candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression) } // In the event that the intersection of supported algorithms is empty we use the ones @@ -428,7 +450,7 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En } for _, key := range encryptKeys { - if err := packet.SerializeEncryptedKey(keyWriter, key.PublicKey, cipher, symKey, config); err != nil { + if err := packet.SerializeEncryptedKeyAEAD(keyWriter, key.PublicKey, cipher, aeadSupported, symKey, config); err != nil { return nil, err } } @@ -465,13 +487,17 @@ func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Con hashToHashId(crypto.SHA3_512), } defaultHashes := candidateHashes[0:1] - preferredHashes := signed.PrimaryIdentity().SelfSignature.PreferredHash + primarySelfSignature, _ := signed.PrimarySelfSignature() + if primarySelfSignature == nil { + return nil, errors.StructuralError("signed entity has no self-signature") + } + preferredHashes := primarySelfSignature.PreferredHash if len(preferredHashes) == 0 { preferredHashes = defaultHashes } candidateHashes = intersectPreferences(candidateHashes, preferredHashes) if len(candidateHashes) == 0 { - return nil, errors.InvalidArgumentError("cannot sign because signing key shares no common algorithms with candidate hashes") + return nil, errors.StructuralError("cannot sign because signing key shares no common algorithms with candidate hashes") } return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, packet.SigTypeBinary, config) @@ -486,6 +512,7 @@ type signatureWriter struct { hashType crypto.Hash wrappedHash hash.Hash h hash.Hash + salt []byte // v6 only signer *packet.PrivateKey sigType packet.SignatureType config *packet.Config @@ -509,6 +536,10 @@ func (s signatureWriter) Close() error { sig.Hash = s.hashType sig.Metadata = s.metadata + if err := sig.SetSalt(s.salt); err != nil { + return err + } + if err := sig.Sign(s.h, s.signer, s.config); err != nil { return err } diff --git a/openpgp/write_test.go b/openpgp/write_test.go index 28f5a96d3..5f9995529 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -8,7 +8,6 @@ import ( "bytes" "crypto/rand" "io" - "io/ioutil" mathrand "math/rand" "testing" "time" @@ -61,7 +60,9 @@ func TestSignDetachedDSA(t *testing.T) { func TestSignDetachedP256(t *testing.T) { kring, _ := ReadKeyRing(readerFromHex(p256TestKeyPrivateHex)) - kring[0].PrivateKey.Decrypt([]byte("passphrase")) + if err := kring[0].PrivateKey.Decrypt([]byte("passphrase")); err != nil { + t.Error(err) + } out := bytes.NewBuffer(nil) message := bytes.NewBufferString(signedInput) @@ -219,7 +220,7 @@ func TestNewEntity(t *testing.T) { el, err := ReadKeyRing(w) if err != nil { - t.Errorf("failed to reparse entity: %s", err) + t.Errorf("failed to rephrase entity: %s", err) return } @@ -253,7 +254,7 @@ func TestNewEntity(t *testing.T) { _, err = ReadKeyRing(w) if err != nil { - t.Errorf("failed to reparse entity after encryption round: %s", err) + t.Errorf("failed to re-parse entity after encryption round: %s", err) return } } @@ -281,7 +282,7 @@ func TestEncryptWithCompression(t *testing.T) { buf := new(bytes.Buffer) var config = &packet.Config{ DefaultCompressionAlgo: packet.CompressionZIP, - CompressionConfig: &packet.CompressionConfig{-1}, + CompressionConfig: &packet.CompressionConfig{Level: -1}, } w, err := Encrypt(buf, kring[:1], nil, nil /* no hints */, config) if err != nil { @@ -350,7 +351,7 @@ func TestSymmetricEncryption(t *testing.T) { } } -func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { +func TestSymmetricEncryptionSEIPDv2RandomizeSlow(t *testing.T) { modesS2K := map[int]s2k.Mode{ 0: s2k.IteratedSaltedS2K, 1: s2k.Argon2S2K, @@ -391,6 +392,9 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { packets := packet.NewReader(copiedBuf) // First a SymmetricKeyEncrypted packet p, err := packets.Next() + if err != nil { + t.Fatal(err) + } switch tp := p.(type) { case *packet.SymmetricKeyEncrypted: default: @@ -398,6 +402,9 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { } // Then an SymmetricallyEncrypted packet version 2 p, err = packets.Next() + if err != nil { + t.Fatal(err) + } switch tp := p.(type) { case *packet.SymmetricallyEncrypted: if tp.Version != 2 { @@ -428,22 +435,27 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { var testEncryptionTests = []struct { keyRingHex string isSigned bool + okV6 bool }{ { testKeys1And2PrivateHex, false, + true, }, { testKeys1And2PrivateHex, true, + true, }, { dsaElGamalTestKeysHex, false, + false, }, { dsaElGamalTestKeysHex, true, + false, }, } @@ -483,7 +495,7 @@ func TestEncryption(t *testing.T) { } compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{level} + compConf := &packet.CompressionConfig{Level: level} var config = &packet.Config{ DefaultCompressionAlgo: compAlgo, CompressionConfig: compConf, @@ -498,6 +510,11 @@ func TestEncryption(t *testing.T) { } w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) + if (err != nil) == (test.okV6 && config.AEAD() != nil) { + // ElGamal is not allowed with v6 + continue + } + if err != nil { t.Errorf("#%d: error in Encrypt: %s", i, err) continue @@ -533,7 +550,7 @@ func TestEncryption(t *testing.T) { } } - plaintext, err := ioutil.ReadAll(md.UnverifiedBody) + plaintext, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("#%d: error reading encrypted contents: %s", i, err) continue @@ -569,6 +586,9 @@ var testSigningTests = []struct { { dsaElGamalTestKeysHex, }, + { + ed25519wX25519Key, + }, } func TestSigning(t *testing.T) { @@ -620,7 +640,7 @@ func TestSigning(t *testing.T) { continue } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") + testTime, _ := time.Parse("2006-01-02", "2022-12-01") signKey, _ := kring[0].SigningKey(testTime) expectedKeyId := signKey.PublicKey.KeyId if md.SignedByKeyId != expectedKeyId { @@ -630,7 +650,7 @@ func TestSigning(t *testing.T) { t.Errorf("#%d: failed to find the signing Entity", i) } - plaintext, err := ioutil.ReadAll(md.UnverifiedBody) + plaintext, err := io.ReadAll(md.UnverifiedBody) if err != nil { t.Errorf("#%d: error reading contents: %v", i, err) continue @@ -744,9 +764,12 @@ FindKey: } decPackets, err := packet.Read(decrypted) + if err != nil { + return err + } _, ok := decPackets.(*packet.Compressed) if !ok { - return errors.InvalidArgumentError("No compressed packets found") + return errors.StructuralError("No compressed packets found") } return nil } diff --git a/openpgp/x25519/x25519.go b/openpgp/x25519/x25519.go new file mode 100644 index 000000000..38afcc74f --- /dev/null +++ b/openpgp/x25519/x25519.go @@ -0,0 +1,221 @@ +package x25519 + +import ( + "crypto/sha256" + "crypto/subtle" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + "github.com/ProtonMail/go-crypto/openpgp/errors" + x25519lib "github.com/cloudflare/circl/dh/x25519" + "golang.org/x/crypto/hkdf" +) + +const ( + hkdfInfo = "OpenPGP X25519" + aes128KeySize = 16 + // The size of a public or private key in bytes. + KeySize = x25519lib.Size +) + +type PublicKey struct { + // Point represents the encoded elliptic curve point of the public key. + Point []byte +} + +type PrivateKey struct { + PublicKey + // Secret represents the secret of the private key. + Secret []byte +} + +// NewPrivateKey creates a new empty private key including the public key. +func NewPrivateKey(key PublicKey) *PrivateKey { + return &PrivateKey{ + PublicKey: key, + } +} + +// Validate validates that the provided public key matches the private key. +func Validate(pk *PrivateKey) (err error) { + var expectedPublicKey, privateKey x25519lib.Key + subtle.ConstantTimeCopy(1, privateKey[:], pk.Secret) + x25519lib.KeyGen(&expectedPublicKey, &privateKey) + if subtle.ConstantTimeCompare(expectedPublicKey[:], pk.PublicKey.Point) == 0 { + return errors.KeyInvalidError("x25519: invalid key") + } + return nil +} + +// GenerateKey generates a new x25519 key pair. +func GenerateKey(rand io.Reader) (*PrivateKey, error) { + var privateKey, publicKey x25519lib.Key + privateKeyOut := new(PrivateKey) + err := generateKey(rand, &privateKey, &publicKey) + if err != nil { + return nil, err + } + privateKeyOut.PublicKey.Point = publicKey[:] + privateKeyOut.Secret = privateKey[:] + return privateKeyOut, nil +} + +func generateKey(rand io.Reader, privateKey *x25519lib.Key, publicKey *x25519lib.Key) error { + maxRounds := 10 + isZero := true + for round := 0; isZero; round++ { + if round == maxRounds { + return errors.InvalidArgumentError("x25519: zero keys only, randomness source might be corrupt") + } + _, err := io.ReadFull(rand, privateKey[:]) + if err != nil { + return err + } + isZero = constantTimeIsZero(privateKey[:]) + } + x25519lib.KeyGen(publicKey, privateKey) + return nil +} + +// Encrypt encrypts a sessionKey with x25519 according to +// the OpenPGP crypto refresh specification section 5.1.6. The function assumes that the +// sessionKey has the correct format and padding according to the specification. +func Encrypt(rand io.Reader, publicKey *PublicKey, sessionKey []byte) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, err error) { + var ephemeralPrivate, ephemeralPublic, staticPublic, shared x25519lib.Key + // Check that the input static public key has 32 bytes + if len(publicKey.Point) != KeySize { + err = errors.KeyInvalidError("x25519: the public key has the wrong size") + return + } + copy(staticPublic[:], publicKey.Point) + // Generate ephemeral keyPair + err = generateKey(rand, &ephemeralPrivate, &ephemeralPublic) + if err != nil { + return + } + // Compute shared key + ok := x25519lib.Shared(&shared, &ephemeralPrivate, &staticPublic) + if !ok { + err = errors.KeyInvalidError("x25519: the public key is a low order point") + return + } + // Derive the encryption key from the shared secret + encryptionKey := applyHKDF(ephemeralPublic[:], publicKey.Point[:], shared[:]) + ephemeralPublicKey = &PublicKey{ + Point: ephemeralPublic[:], + } + // Encrypt the sessionKey with aes key wrapping + encryptedSessionKey, err = keywrap.Wrap(encryptionKey, sessionKey) + return +} + +// Decrypt decrypts a session key stored in ciphertext with the provided x25519 +// private key and ephemeral public key. +func Decrypt(privateKey *PrivateKey, ephemeralPublicKey *PublicKey, ciphertext []byte) (encodedSessionKey []byte, err error) { + var ephemeralPublic, staticPrivate, shared x25519lib.Key + // Check that the input ephemeral public key has 32 bytes + if len(ephemeralPublicKey.Point) != KeySize { + err = errors.KeyInvalidError("x25519: the public key has the wrong size") + return + } + copy(ephemeralPublic[:], ephemeralPublicKey.Point) + subtle.ConstantTimeCopy(1, staticPrivate[:], privateKey.Secret) + // Compute shared key + ok := x25519lib.Shared(&shared, &staticPrivate, &ephemeralPublic) + if !ok { + err = errors.KeyInvalidError("x25519: the ephemeral public key is a low order point") + return + } + // Derive the encryption key from the shared secret + encryptionKey := applyHKDF(ephemeralPublicKey.Point[:], privateKey.PublicKey.Point[:], shared[:]) + // Decrypt the session key with aes key wrapping + encodedSessionKey, err = keywrap.Unwrap(encryptionKey, ciphertext) + return +} + +func applyHKDF(ephemeralPublicKey []byte, publicKey []byte, sharedSecret []byte) []byte { + inputKey := make([]byte, 3*KeySize) + // ephemeral public key | recipient public key | shared secret + subtle.ConstantTimeCopy(1, inputKey[:KeySize], ephemeralPublicKey) + subtle.ConstantTimeCopy(1, inputKey[KeySize:2*KeySize], publicKey) + subtle.ConstantTimeCopy(1, inputKey[2*KeySize:], sharedSecret) + hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, []byte(hkdfInfo)) + encryptionKey := make([]byte, aes128KeySize) + _, _ = io.ReadFull(hkdfReader, encryptionKey) + return encryptionKey +} + +func constantTimeIsZero(bytes []byte) bool { + isZero := byte(0) + for _, b := range bytes { + isZero |= b + } + return isZero == 0 +} + +// ENCODING/DECODING ciphertexts: + +// EncodeFieldsLength returns the length of the ciphertext encoding +// given the encrypted session key. +func EncodedFieldsLength(encryptedSessionKey []byte, v6 bool) int { + lenCipherFunction := 0 + if !v6 { + lenCipherFunction = 1 + } + return KeySize + 1 + len(encryptedSessionKey) + lenCipherFunction +} + +// EncodeField encodes x25519 session key encryption fields as +// ephemeral x25519 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey +// and writes it to writer. +func EncodeFields(writer io.Writer, ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) { + lenAlgorithm := 0 + if !v6 { + lenAlgorithm = 1 + } + if _, err = writer.Write(ephemeralPublicKey.Point); err != nil { + return err + } + if _, err = writer.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil { + return err + } + if !v6 { + if _, err = writer.Write([]byte{cipherFunction}); err != nil { + return err + } + } + _, err = writer.Write(encryptedSessionKey) + return err +} + +// DecodeField decodes a x25519 session key encryption as +// ephemeral x25519 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey. +func DecodeFields(reader io.Reader, v6 bool) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, err error) { + var buf [1]byte + ephemeralPublicKey = &PublicKey{ + Point: make([]byte, KeySize), + } + // 32 octets representing an ephemeral x25519 public key. + if _, err = io.ReadFull(reader, ephemeralPublicKey.Point); err != nil { + return nil, nil, 0, err + } + // A one-octet size of the following fields. + if _, err = io.ReadFull(reader, buf[:]); err != nil { + return nil, nil, 0, err + } + followingLen := buf[0] + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + if !v6 { + if _, err = io.ReadFull(reader, buf[:]); err != nil { + return nil, nil, 0, err + } + cipherFunction = buf[0] + followingLen -= 1 + } + // The encrypted session key. + encryptedSessionKey = make([]byte, followingLen) + if _, err = io.ReadFull(reader, encryptedSessionKey); err != nil { + return nil, nil, 0, err + } + return ephemeralPublicKey, encryptedSessionKey, cipherFunction, nil +} diff --git a/openpgp/x25519/x25519_test.go b/openpgp/x25519/x25519_test.go new file mode 100644 index 000000000..d66d8fb91 --- /dev/null +++ b/openpgp/x25519/x25519_test.go @@ -0,0 +1,59 @@ +package x25519 + +import ( + "bytes" + "crypto/rand" + "testing" +) + +func TestGenerate(t *testing.T) { + privateKey, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + if len(privateKey.Secret) != KeySize { + t.Fatal("key has the wrong size") + } + if len(privateKey.PublicKey.Point) != KeySize { + t.Fatal("key has the wrong size") + } +} + +func TestValidate(t *testing.T) { + privateKey, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + err = Validate(privateKey) + if err != nil { + t.Fatal(err) + } + privateKey.PublicKey.Point[0] = privateKey.PublicKey.Point[0] + byte(1) + err = Validate(privateKey) + if err == nil { + t.Fatal("validation failed") + } +} + +func TestEncryptDecrypt(t *testing.T) { + sessionKey := []byte("session.........") + privateKey, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + ephemeralPublic, ctxt, err := Encrypt(rand.Reader, &privateKey.PublicKey, sessionKey) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + sessionKeyAfter, err := Decrypt(privateKey, ephemeralPublic, ctxt) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + + if !bytes.Equal(sessionKeyAfter, sessionKey) { + t.Errorf("decryption failed, got: %x, want: %x", sessionKeyAfter, sessionKey) + } +} diff --git a/openpgp/x448/x448.go b/openpgp/x448/x448.go new file mode 100644 index 000000000..65a082dab --- /dev/null +++ b/openpgp/x448/x448.go @@ -0,0 +1,229 @@ +package x448 + +import ( + "crypto/sha512" + "crypto/subtle" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + "github.com/ProtonMail/go-crypto/openpgp/errors" + x448lib "github.com/cloudflare/circl/dh/x448" + "golang.org/x/crypto/hkdf" +) + +const ( + hkdfInfo = "OpenPGP X448" + aes256KeySize = 32 + // The size of a public or private key in bytes. + KeySize = x448lib.Size +) + +type PublicKey struct { + // Point represents the encoded elliptic curve point of the public key. + Point []byte +} + +type PrivateKey struct { + PublicKey + // Secret represents the secret of the private key. + Secret []byte +} + +// NewPrivateKey creates a new empty private key including the public key. +func NewPrivateKey(key PublicKey) *PrivateKey { + return &PrivateKey{ + PublicKey: key, + } +} + +// Validate validates that the provided public key matches +// the private key. +func Validate(pk *PrivateKey) (err error) { + var expectedPublicKey, privateKey x448lib.Key + subtle.ConstantTimeCopy(1, privateKey[:], pk.Secret) + x448lib.KeyGen(&expectedPublicKey, &privateKey) + if subtle.ConstantTimeCompare(expectedPublicKey[:], pk.PublicKey.Point) == 0 { + return errors.KeyInvalidError("x448: invalid key") + } + return nil +} + +// GenerateKey generates a new x448 key pair. +func GenerateKey(rand io.Reader) (*PrivateKey, error) { + var privateKey, publicKey x448lib.Key + privateKeyOut := new(PrivateKey) + err := generateKey(rand, &privateKey, &publicKey) + if err != nil { + return nil, err + } + privateKeyOut.PublicKey.Point = publicKey[:] + privateKeyOut.Secret = privateKey[:] + return privateKeyOut, nil +} + +func generateKey(rand io.Reader, privateKey *x448lib.Key, publicKey *x448lib.Key) error { + maxRounds := 10 + isZero := true + for round := 0; isZero; round++ { + if round == maxRounds { + return errors.InvalidArgumentError("x448: zero keys only, randomness source might be corrupt") + } + _, err := io.ReadFull(rand, privateKey[:]) + if err != nil { + return err + } + isZero = constantTimeIsZero(privateKey[:]) + } + x448lib.KeyGen(publicKey, privateKey) + return nil +} + +// Encrypt encrypts a sessionKey with x448 according to +// the OpenPGP crypto refresh specification section 5.1.7. The function assumes that the +// sessionKey has the correct format and padding according to the specification. +func Encrypt(rand io.Reader, publicKey *PublicKey, sessionKey []byte) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, err error) { + var ephemeralPrivate, ephemeralPublic, staticPublic, shared x448lib.Key + // Check that the input static public key has 56 bytes. + if len(publicKey.Point) != KeySize { + err = errors.KeyInvalidError("x448: the public key has the wrong size") + return nil, nil, err + } + copy(staticPublic[:], publicKey.Point) + // Generate ephemeral keyPair. + if err = generateKey(rand, &ephemeralPrivate, &ephemeralPublic); err != nil { + return nil, nil, err + } + // Compute shared key. + ok := x448lib.Shared(&shared, &ephemeralPrivate, &staticPublic) + if !ok { + err = errors.KeyInvalidError("x448: the public key is a low order point") + return nil, nil, err + } + // Derive the encryption key from the shared secret. + encryptionKey := applyHKDF(ephemeralPublic[:], publicKey.Point[:], shared[:]) + ephemeralPublicKey = &PublicKey{ + Point: ephemeralPublic[:], + } + // Encrypt the sessionKey with aes key wrapping. + encryptedSessionKey, err = keywrap.Wrap(encryptionKey, sessionKey) + if err != nil { + return nil, nil, err + } + return ephemeralPublicKey, encryptedSessionKey, nil +} + +// Decrypt decrypts a session key stored in ciphertext with the provided x448 +// private key and ephemeral public key. +func Decrypt(privateKey *PrivateKey, ephemeralPublicKey *PublicKey, ciphertext []byte) (encodedSessionKey []byte, err error) { + var ephemeralPublic, staticPrivate, shared x448lib.Key + // Check that the input ephemeral public key has 56 bytes. + if len(ephemeralPublicKey.Point) != KeySize { + err = errors.KeyInvalidError("x448: the public key has the wrong size") + return nil, err + } + copy(ephemeralPublic[:], ephemeralPublicKey.Point) + subtle.ConstantTimeCopy(1, staticPrivate[:], privateKey.Secret) + // Compute shared key. + ok := x448lib.Shared(&shared, &staticPrivate, &ephemeralPublic) + if !ok { + err = errors.KeyInvalidError("x448: the ephemeral public key is a low order point") + return nil, err + } + // Derive the encryption key from the shared secret. + encryptionKey := applyHKDF(ephemeralPublicKey.Point[:], privateKey.PublicKey.Point[:], shared[:]) + // Decrypt the session key with aes key wrapping. + encodedSessionKey, err = keywrap.Unwrap(encryptionKey, ciphertext) + if err != nil { + return nil, err + } + return encodedSessionKey, nil +} + +func applyHKDF(ephemeralPublicKey []byte, publicKey []byte, sharedSecret []byte) []byte { + inputKey := make([]byte, 3*KeySize) + // ephemeral public key | recipient public key | shared secret. + subtle.ConstantTimeCopy(1, inputKey[:KeySize], ephemeralPublicKey) + subtle.ConstantTimeCopy(1, inputKey[KeySize:2*KeySize], publicKey) + subtle.ConstantTimeCopy(1, inputKey[2*KeySize:], sharedSecret) + hkdfReader := hkdf.New(sha512.New, inputKey, []byte{}, []byte(hkdfInfo)) + encryptionKey := make([]byte, aes256KeySize) + _, _ = io.ReadFull(hkdfReader, encryptionKey) + return encryptionKey +} + +func constantTimeIsZero(bytes []byte) bool { + isZero := byte(0) + for _, b := range bytes { + isZero |= b + } + return isZero == 0 +} + +// ENCODING/DECODING ciphertexts: + +// EncodeFieldsLength returns the length of the ciphertext encoding +// given the encrypted session key. +func EncodedFieldsLength(encryptedSessionKey []byte, v6 bool) int { + lenCipherFunction := 0 + if !v6 { + lenCipherFunction = 1 + } + return KeySize + 1 + len(encryptedSessionKey) + lenCipherFunction +} + +// EncodeField encodes x448 session key encryption fields as +// ephemeral x448 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey +// and writes it to writer. +func EncodeFields(writer io.Writer, ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) { + lenAlgorithm := 0 + if !v6 { + lenAlgorithm = 1 + } + if _, err = writer.Write(ephemeralPublicKey.Point); err != nil { + return err + } + if _, err = writer.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil { + return err + } + if !v6 { + if _, err = writer.Write([]byte{cipherFunction}); err != nil { + return err + } + } + if _, err = writer.Write(encryptedSessionKey); err != nil { + return err + } + return nil +} + +// DecodeField decodes a x448 session key encryption as +// ephemeral x448 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey. +func DecodeFields(reader io.Reader, v6 bool) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, err error) { + var buf [1]byte + ephemeralPublicKey = &PublicKey{ + Point: make([]byte, KeySize), + } + // 56 octets representing an ephemeral x448 public key. + if _, err = io.ReadFull(reader, ephemeralPublicKey.Point); err != nil { + return nil, nil, 0, err + } + // A one-octet size of the following fields. + if _, err = io.ReadFull(reader, buf[:]); err != nil { + return nil, nil, 0, err + } + followingLen := buf[0] + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + if !v6 { + if _, err = io.ReadFull(reader, buf[:]); err != nil { + return nil, nil, 0, err + } + cipherFunction = buf[0] + followingLen -= 1 + } + // The encrypted session key. + encryptedSessionKey = make([]byte, followingLen) + if _, err = io.ReadFull(reader, encryptedSessionKey); err != nil { + return nil, nil, 0, err + } + return ephemeralPublicKey, encryptedSessionKey, cipherFunction, nil +} diff --git a/openpgp/x448/x448_test.go b/openpgp/x448/x448_test.go new file mode 100644 index 000000000..8b3e74db0 --- /dev/null +++ b/openpgp/x448/x448_test.go @@ -0,0 +1,59 @@ +package x448 + +import ( + "bytes" + "crypto/rand" + "testing" +) + +func TestGenerate(t *testing.T) { + privateKey, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + if len(privateKey.Secret) != KeySize { + t.Fatal("key has the wrong size") + } + if len(privateKey.PublicKey.Point) != KeySize { + t.Fatal("key has the wrong size") + } +} + +func TestValidate(t *testing.T) { + privateKey, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + err = Validate(privateKey) + if err != nil { + t.Fatal(err) + } + privateKey.PublicKey.Point[0] = privateKey.PublicKey.Point[0] + byte(1) + err = Validate(privateKey) + if err == nil { + t.Fatal("validation failed") + } +} + +func TestEncryptDecrypt(t *testing.T) { + sessionKey := []byte("session.........") + privateKey, err := GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + ephemeralPublic, ctxt, err := Encrypt(rand.Reader, &privateKey.PublicKey, sessionKey) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + sessionKeyAfter, err := Decrypt(privateKey, ephemeralPublic, ctxt) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + + if !bytes.Equal(sessionKeyAfter, sessionKey) { + t.Errorf("decryption failed, got: %x, want: %x", sessionKeyAfter, sessionKey) + } +}