From 60f6f3e9a98263cc2c51ebe1f9babe82ded3f0ba Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Fri, 15 Oct 2021 13:40:04 +0200 Subject: [PATCH] Handle signing-only subkeys (#112) Co-authored-by: CrazyMax --- .github/workflows/ci.yml | 52 +++-- README.md | 1 + __tests__/context.test.ts | 1 - __tests__/fixtures/gpg.conf | 71 +++++++ .../fixtures}/test-key-base64.pgp | 0 {.github => __tests__/fixtures}/test-key.pass | 0 {.github => __tests__/fixtures}/test-key.pgp | 0 __tests__/fixtures/test-subkey-base64.pgp | 1 + __tests__/fixtures/test-subkey.pass | 1 + __tests__/fixtures/test-subkey.pgp | 19 ++ __tests__/gpg.test.ts | 187 ++++++++++-------- __tests__/openpgp.test.ts | 131 +++++++----- action.yml | 3 + dist/index.js | 51 +++-- src/context.ts | 4 +- src/main.ts | 34 ++-- src/openpgp.ts | 5 +- src/state-helper.ts | 6 +- 18 files changed, 375 insertions(+), 192 deletions(-) create mode 100644 __tests__/fixtures/gpg.conf rename {.github => __tests__/fixtures}/test-key-base64.pgp (100%) rename {.github => __tests__/fixtures}/test-key.pass (100%) rename {.github => __tests__/fixtures}/test-key.pgp (100%) create mode 100644 __tests__/fixtures/test-subkey-base64.pgp create mode 100644 __tests__/fixtures/test-subkey.pass create mode 100644 __tests__/fixtures/test-subkey.pgp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63e68b1c..dc57b8ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: ci on: schedule: - - cron: '0 10 * * *' # everyday at 10am + - cron: '0 10 * * *' push: branches: - 'master' @@ -20,6 +20,9 @@ jobs: strategy: fail-fast: false matrix: + key: + - test-key + - test-subkey global: - false - true @@ -27,10 +30,26 @@ jobs: - ubuntu-latest - macOS-latest - windows-latest + include: + - key: test-subkey + fingerprint: C17D11ADF199F12A30A0910F1F80449BE0B08CB8 steps: - name: Checkout uses: actions/checkout@v2 + - + name: GPG conf + uses: actions/github-script@v4 + with: + script: | + const fs = require('fs'); + const gnupgfolder = `${require('os').homedir()}/.gnupg`; + if (!fs.existsSync(gnupgfolder)){ + fs.mkdirSync(gnupgfolder); + } + fs.copyFile('__tests__/fixtures/gpg.conf', `${gnupgfolder}/gpg.conf`, (err) => { + if (err) throw err; + }); - name: Get test key and passphrase uses: actions/github-script@v5 @@ -38,8 +57,8 @@ jobs: with: script: | const fs = require('fs'); - core.setOutput('pgp', fs.readFileSync('.github/test-key.pgp', {encoding: 'utf8'})); - core.setOutput('passphrase', fs.readFileSync('.github/test-key.pass', {encoding: 'utf8'})); + core.setOutput('pgp', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pgp', {encoding: 'utf8'})); + core.setOutput('passphrase', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pass', {encoding: 'utf8'})); - name: Import GPG id: import_gpg @@ -52,23 +71,28 @@ jobs: git_commit_gpgsign: true git_tag_gpgsign: true git_push_gpgsign: if-asked + fingerprint: ${{ matrix.fingerprint }} - - name: GPG user IDs + name: List keys run: | - echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}" - echo "keyid: ${{ steps.import_gpg.outputs.keyid }}" - echo "name: ${{ steps.import_gpg.outputs.name }}" - echo "email: ${{ steps.import_gpg.outputs.email }}" + gpg -K + shell: bash base64: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + key: + - test-key + - test-subkey os: - ubuntu-latest - macOS-latest - windows-latest + include: + - key: test-subkey + fingerprint: C17D11ADF199F12A30A0910F1F80449BE0B08CB8 steps: - name: Checkout @@ -80,8 +104,8 @@ jobs: with: script: | const fs = require('fs'); - core.setOutput('pgp-base64', fs.readFileSync('.github/test-key-base64.pgp', {encoding: 'utf8'})); - core.setOutput('passphrase', fs.readFileSync('.github/test-key.pass', {encoding: 'utf8'})); + core.setOutput('pgp-base64', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}-base64.pgp', {encoding: 'utf8'})); + core.setOutput('passphrase', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pass', {encoding: 'utf8'})); - name: Import GPG id: import_gpg @@ -93,10 +117,4 @@ jobs: git_commit_gpgsign: true git_tag_gpgsign: true git_push_gpgsign: if-asked - - - name: GPG user IDs - run: | - echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}" - echo "keyid: ${{ steps.import_gpg.outputs.keyid }}" - echo "name: ${{ steps.import_gpg.outputs.name }}" - echo "email: ${{ steps.import_gpg.outputs.email }}" + fingerprint: ${{ matrix.fingerprint }} diff --git a/README.md b/README.md index d2728337..3c4b0ba9 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Following inputs can be used as `step.with` keys | `git_committer_name` | String | Set commit author's name (defaults to the name associated with the GPG key) | | `git_committer_email` | String | Set commit author's email (defaults to the email address associated with the GPG key) | | `workdir` | String | Working directory (below repository root) (default `.`) | +| `fingerprint` | String | Specific fingerprint to use (subkey) | > `git_user_signingkey` needs to be enabled for `git_commit_gpgsign`, `git_tag_gpgsign`, > `git_push_gpgsign`, `git_committer_name`, `git_committer_email` inputs. diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index b5ce77bb..08df82cf 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -1,5 +1,4 @@ import * as os from 'os'; - import * as context from '../src/context'; describe('setOutput', () => { diff --git a/__tests__/fixtures/gpg.conf b/__tests__/fixtures/gpg.conf new file mode 100644 index 00000000..4418c056 --- /dev/null +++ b/__tests__/fixtures/gpg.conf @@ -0,0 +1,71 @@ +################################################################################ +# GnuPG Options + +# (OpenPGP-Configuration-Options) +# Assume that command line arguments are given as UTF8 strings. +utf8-strings + +# (OpenPGP-Protocol-Options) +# Set the list of personal digest/cipher/compression preferences. This allows +# the user to safely override the algorithm chosen by the recipient key +# preferences, as GPG will only select an algorithm that is usable by all +# recipients. +personal-digest-preferences SHA512 SHA384 SHA256 SHA224 +personal-cipher-preferences AES256 AES192 AES CAST5 CAMELLIA192 BLOWFISH TWOFISH CAMELLIA128 3DES +personal-compress-preferences ZLIB BZIP2 ZIP + +# Set the list of default preferences to string. This preference list is used +# for new keys and becomes the default for "setpref" in the edit menu. +default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed + +# (OpenPGP-Esoteric-Options) +# Use name as the message digest algorithm used when signing a key. Running the +# program with the command --version yields a list of supported algorithms. Be +# aware that if you choose an algorithm that GnuPG supports but other OpenPGP +# implementations do not, then some users will not be able to use the key +# signatures you make, or quite possibly your entire key. +# +# SHA-1 is the only algorithm specified for OpenPGP V4. By changing the +# cert-digest-algo, the OpenPGP V4 specification is not met but with even +# GnuPG 1.4.10 (release 2009) supporting SHA-2 algorithm, this should be safe. +# Source: https://tools.ietf.org/html/rfc4880#section-12.2 +cert-digest-algo SHA512 +digest-algo SHA256 + +# Selects how passphrases for symmetric encryption are mangled. 3 (the default) +# iterates the whole process a number of times (see --s2k-count). +s2k-mode 3 + +# (OpenPGP-Protocol-Options) +# Use name as the cipher algorithm for symmetric encryption with a passphrase +# if --personal-cipher-preferences and --cipher-algo are not given. The +# default is AES-128. +s2k-cipher-algo AES256 + +# (OpenPGP-Protocol-Options) +# Use name as the digest algorithm used to mangle the passphrases for symmetric +# encryption. The default is SHA-1. +s2k-digest-algo SHA512 + +# (OpenPGP-Protocol-Options) +# Specify how many times the passphrases mangling for symmetric encryption is +# repeated. This value may range between 1024 and 65011712 inclusive. The +# default is inquired from gpg-agent. Note that not all values in the +# 1024-65011712 range are legal and if an illegal value is selected, GnuPG will +# round up to the nearest legal value. This option is only meaningful if +# --s2k-mode is set to the default of 3. +s2k-count 1015808 + +################################################################################ +# GnuPG View Options + +# Select how to display key IDs. "long" is the more accurate (but less +# convenient) 16-character key ID. Add an "0x" to include an "0x" at the +# beginning of the key ID. +keyid-format 0xlong + +# List all keys with their fingerprints. This is the same output as --list-keys +# but with the additional output of a line with the fingerprint. If this +# command is given twice, the fingerprints of all secondary keys are listed too. +with-fingerprint +with-fingerprint diff --git a/.github/test-key-base64.pgp b/__tests__/fixtures/test-key-base64.pgp similarity index 100% rename from .github/test-key-base64.pgp rename to __tests__/fixtures/test-key-base64.pgp diff --git a/.github/test-key.pass b/__tests__/fixtures/test-key.pass similarity index 100% rename from .github/test-key.pass rename to __tests__/fixtures/test-key.pass diff --git a/.github/test-key.pgp b/__tests__/fixtures/test-key.pgp similarity index 100% rename from .github/test-key.pgp rename to __tests__/fixtures/test-key.pgp diff --git a/__tests__/fixtures/test-subkey-base64.pgp b/__tests__/fixtures/test-subkey-base64.pgp new file mode 100644 index 00000000..013632a5 --- /dev/null +++ b/__tests__/fixtures/test-subkey-base64.pgp @@ -0,0 +1 @@ +LS0tLS1CRUdJTiBQR1AgUFJJVkFURSBLRVkgQkxPQ0stLS0tLQoKbElZRVlVNU0yaFlKS3dZQkJBSGFSdzhCQVFkQXNSbDlDUEtaaDB4MC9FRDFveDJwTmJ6R1J1TlpvRlVSN0JsYgpOUUdabzB6K0J3TUN1dVdvaTR5WTQ0YkhNU1AwMjBLRmUvOHhpWHJwby9LandiMXJaa1g3dW1laWZBRFh6L1JiCmJuMXdKMENGQ09TOHl4R3laL3NCYlk1OGZEL0gvMFU2TFdiSmRHSG1mZ0RXYTl0OEFQK09NTFFWU205bElFSmgKY2lBOGFtOWxRR0poY2k1bWIyOCtpSkFFRXhZS0FEZ1dJUVNIOGxlNG5PUmlFQXZzRC81Z2NkSVlPQS9jeUFVQwpZVTVNMmdJYkFRVUxDUWdIQXdVVkNna0lDd1VXQWdNQkFBSWVBUUlYZ0FBS0NSQmdjZElZT0EvY3lGd1VBUUN0CmRQdzU3MDh0Z296NkNqcEFMbzBjQ2NtZ2xuVHdGWlBYTm1DaGdPZUIzQUVBdkdNV2lrYy9iaG9waVRGUzNLVWkKR042a1o5ZUlhaTRYeDloTjRSZTlEd1NjaGdSaFRrMURGZ2tyQmdFRUFkcEhEd0VCQjBDVUtPdVVMYlNqZVF4QwpHNmY4VkhNWHRUbnc4MkF2TmlwM01rY3RNZEZmbC80SEF3SlBPM1loUVJkWU44Y1A1cVhvOFcwazFPZEJaTEJyCmN5cm5ra2tYVk91cjh1SlExV2tMb2FMSnZ3VmN1MlplSFlWdmcramNFSmVlTVF0ME43OWVOUUs5VVMzeEQ5ak4Kc2JZbTVrUkNHWldpaU84RUdCWUtBQ0FXSVFTSDhsZTRuT1JpRUF2c0QvNWdjZElZT0EvY3lBVUNZVTVOUXdJYgpBZ0NCQ1JCZ2NkSVlPQS9jeUhZZ0JCa1dDZ0FkRmlFRXdYMFJyZkdaOFNvd29KRVBINEJFbStDd2pMZ0ZBbUZPClRVTUFDZ2tRSDRCRW0rQ3dqTGlJT1FFQTZjazVCbXMwYzBvbHV4Ly9BeUprMlpINWl5WW11WmpaVTJNOEhtcEoKa1BJQkFPVWJsQmlwZURpc0dqQ0VmTE1SN1czcFBYTTMyY0ZOWVdwOW1SNzJ6SWdOcEdvQS8zM1grRG55VHhtTgpYeUlpZFFtK0J3TFBZOXRTUlMvL0dCbVg4eHdDUWpWS0FRRG54V0VyaVk4clBQOTFUblhtR0VjL05LeFZVcHJoCjVRTndjMHNBTjVGRUJ3PT0KPTExQjQKLS0tLS1FTkQgUEdQIFBSSVZBVEUgS0VZIEJMT0NLLS0tLS0K \ No newline at end of file diff --git a/__tests__/fixtures/test-subkey.pass b/__tests__/fixtures/test-subkey.pass new file mode 100644 index 00000000..fbeb5a61 --- /dev/null +++ b/__tests__/fixtures/test-subkey.pass @@ -0,0 +1 @@ +with another passphrase \ No newline at end of file diff --git a/__tests__/fixtures/test-subkey.pgp b/__tests__/fixtures/test-subkey.pgp new file mode 100644 index 00000000..4bdc264f --- /dev/null +++ b/__tests__/fixtures/test-subkey.pgp @@ -0,0 +1,19 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEYU5M2hYJKwYBBAHaRw8BAQdAsRl9CPKZh0x0/ED1ox2pNbzGRuNZoFUR7Blb +NQGZo0z+BwMCuuWoi4yY44bHMSP020KFe/8xiXrpo/Kjwb1rZkX7umeifADXz/Rb +bn1wJ0CFCOS8yxGyZ/sBbY58fD/H/0U6LWbJdGHmfgDWa9t8AP+OMLQVSm9lIEJh +ciA8am9lQGJhci5mb28+iJAEExYKADgWIQSH8le4nORiEAvsD/5gcdIYOA/cyAUC +YU5M2gIbAQULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRBgcdIYOA/cyFwUAQCt +dPw5708tgoz6CjpALo0cCcmglnTwFZPXNmChgOeB3AEAvGMWikc/bhopiTFS3KUi +GN6kZ9eIai4Xx9hN4Re9DwSchgRhTk1DFgkrBgEEAdpHDwEBB0CUKOuULbSjeQxC +G6f8VHMXtTnw82AvNip3MkctMdFfl/4HAwJPO3YhQRdYN8cP5qXo8W0k1OdBZLBr +cyrnkkkXVOur8uJQ1WkLoaLJvwVcu2ZeHYVvg+jcEJeeMQt0N79eNQK9US3xD9jN +sbYm5kRCGZWiiO8EGBYKACAWIQSH8le4nORiEAvsD/5gcdIYOA/cyAUCYU5NQwIb +AgCBCRBgcdIYOA/cyHYgBBkWCgAdFiEEwX0RrfGZ8SowoJEPH4BEm+CwjLgFAmFO +TUMACgkQH4BEm+CwjLiIOQEA6ck5Bms0c0olux//AyJk2ZH5iyYmuZjZU2M8HmpJ +kPIBAOUblBipeDisGjCEfLMR7W3pPXM32cFNYWp9mR72zIgNpGoA/33X+DnyTxmN +XyIidQm+BwLPY9tSRS//GBmX8xwCQjVKAQDnxWEriY8rPP91TnXmGEc/NKxVUprh +5QNwc0sAN5FEBw== +=11B4 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/__tests__/gpg.test.ts b/__tests__/gpg.test.ts index d98b19ef..4c810cc7 100644 --- a/__tests__/gpg.test.ts +++ b/__tests__/gpg.test.ts @@ -1,105 +1,130 @@ import * as fs from 'fs'; import * as gpg from '../src/gpg'; -const userInfo = { - pgp: fs.readFileSync('.github/test-key.pgp', { - encoding: 'utf8', - flag: 'r' - }), - pgp_base64: fs.readFileSync('.github/test-key-base64.pgp', { - encoding: 'utf8', - flag: 'r' - }), - passphrase: fs.readFileSync('.github/test-key.pass', { - encoding: 'utf8', - flag: 'r' - }), - name: 'Joe Tester', - email: 'joe@foo.bar', - keyID: 'D523BD50DD70B0BA', - fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', - keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163'] -}; +const userInfos = [ + { + key: 'test-key', + pgp: fs.readFileSync('__tests__/fixtures/test-key.pgp', { + encoding: 'utf8', + flag: 'r' + }), + pgp_base64: fs.readFileSync('__tests__/fixtures/test-key-base64.pgp', { + encoding: 'utf8', + flag: 'r' + }), + passphrase: fs.readFileSync('__tests__/fixtures/test-key.pass', { + encoding: 'utf8', + flag: 'r' + }), + name: 'Joe Tester', + email: 'joe@foo.bar', + keyID: '7D851EB72D73BDA0', + fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', + keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163'] + }, + { + key: 'test-subkey', + pgp: fs.readFileSync('__tests__/fixtures/test-subkey.pgp', { + encoding: 'utf8', + flag: 'r' + }), + pgp_base64: fs.readFileSync('__tests__/fixtures/test-subkey-base64.pgp', { + encoding: 'utf8', + flag: 'r' + }), + passphrase: fs.readFileSync('__tests__/fixtures/test-subkey.pass', { + encoding: 'utf8', + flag: 'r' + }), + name: 'Joe Bar', + email: 'joe@bar.foo', + keyID: '6071D218380FDCC8', + fingerprint: 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8', + keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB'] + } +]; -describe('gpg', () => { - describe('getVersion', () => { - it('returns GnuPG and libgcrypt version', async () => { - await gpg.getVersion().then(version => { - console.log(version); - expect(version.gnupg).not.toEqual(''); - expect(version.libgcrypt).not.toEqual(''); - }); +describe('getVersion', () => { + it('returns GnuPG and libgcrypt version', async () => { + await gpg.getVersion().then(version => { + console.log(version); + expect(version.gnupg).not.toEqual(''); + expect(version.libgcrypt).not.toEqual(''); }); }); +}); - describe('getDirs', () => { - it('returns GnuPG dirs', async () => { - await gpg.getDirs().then(dirs => { - console.log(dirs); - expect(dirs.libdir).not.toEqual(''); - expect(dirs.datadir).not.toEqual(''); - expect(dirs.homedir).not.toEqual(''); - }); +describe('getDirs', () => { + it('returns GnuPG dirs', async () => { + await gpg.getDirs().then(dirs => { + console.log(dirs); + expect(dirs.libdir).not.toEqual(''); + expect(dirs.datadir).not.toEqual(''); + expect(dirs.homedir).not.toEqual(''); }); }); +}); - describe('importKey', () => { - it('imports key (as armored string) to GnuPG', async () => { - await gpg.importKey(userInfo.pgp).then(output => { - console.log(output); - expect(output).not.toEqual(''); - }); - }); - it('imports key (as base64 string) to GnuPG', async () => { - await gpg.importKey(userInfo.pgp_base64).then(output => { - console.log(output); - expect(output).not.toEqual(''); - }); - }); +describe('configureAgent', () => { + it('configures GnuPG agent', async () => { + await gpg.configureAgent(gpg.agentConfig); }); +}); - describe('getKeygrips', () => { - it('returns the keygrips', async () => { - await gpg.importKey(userInfo.pgp); - await gpg.getKeygrips(userInfo.fingerprint).then(keygrips => { - console.log(keygrips); - expect(keygrips.length).toEqual(userInfo.keygrips.length); - for (let i = 0; i < keygrips.length; i++) { - expect(keygrips[i]).toEqual(userInfo.keygrips[i]); - } +for (let userInfo of userInfos) { + describe(userInfo.key, () => { + describe('importKey', () => { + it('imports key (as armored string) to GnuPG', async () => { + await gpg.importKey(userInfo.pgp).then(output => { + console.log(output); + expect(output).not.toEqual(''); + }); + }); + it('imports key (as base64 string) to GnuPG', async () => { + await gpg.importKey(userInfo.pgp_base64).then(output => { + console.log(output); + expect(output).not.toEqual(''); + }); }); }); - }); - describe('configureAgent', () => { - it('configures GnuPG agent', async () => { - await gpg.configureAgent(gpg.agentConfig); + describe('getKeygrips', () => { + it('returns the keygrips', async () => { + await gpg.importKey(userInfo.pgp); + await gpg.getKeygrips(userInfo.fingerprint).then(keygrips => { + console.log(keygrips); + expect(keygrips.length).toEqual(userInfo.keygrips.length); + for (let i = 0; i < keygrips.length; i++) { + expect(keygrips[i]).toEqual(userInfo.keygrips[i]); + } + }); + }); }); - }); - describe('presetPassphrase', () => { - it('presets passphrase', async () => { - await gpg.importKey(userInfo.pgp); - await gpg.configureAgent(gpg.agentConfig); - for (let keygrip of await gpg.getKeygrips(userInfo.fingerprint)) { - await gpg.presetPassphrase(keygrip, userInfo.passphrase).then(output => { - console.log(output); - expect(output).not.toEqual(''); - }); - } + describe('presetPassphrase', () => { + it('presets passphrase', async () => { + await gpg.importKey(userInfo.pgp); + await gpg.configureAgent(gpg.agentConfig); + for (let keygrip of await gpg.getKeygrips(userInfo.fingerprint)) { + await gpg.presetPassphrase(keygrip, userInfo.passphrase).then(output => { + console.log(output); + expect(output).not.toEqual(''); + }); + } + }); }); - }); - describe('deleteKey', () => { - it('removes key from GnuPG', async () => { - await gpg.importKey(userInfo.pgp); - await gpg.deleteKey(userInfo.fingerprint); + describe('deleteKey', () => { + it('removes key from GnuPG', async () => { + await gpg.importKey(userInfo.pgp); + await gpg.deleteKey(userInfo.fingerprint); + }); }); }); +} - describe('killAgent', () => { - it('kills GnuPG agent', async () => { - await gpg.killAgent(); - }); +describe('killAgent', () => { + it('kills GnuPG agent', async () => { + await gpg.killAgent(); }); }); diff --git a/__tests__/openpgp.test.ts b/__tests__/openpgp.test.ts index 23eb506d..898298f4 100644 --- a/__tests__/openpgp.test.ts +++ b/__tests__/openpgp.test.ts @@ -1,66 +1,91 @@ import * as fs from 'fs'; import * as openpgp from '../src/openpgp'; -const userInfo = { - pgp: fs.readFileSync('.github/test-key.pgp', { - encoding: 'utf8', - flag: 'r' - }), - pgp_base64: fs.readFileSync('.github/test-key-base64.pgp', { - encoding: 'utf8', - flag: 'r' - }), - passphrase: fs.readFileSync('.github/test-key.pass', { - encoding: 'utf8', - flag: 'r' - }), - name: 'Joe Tester', - email: 'joe@foo.bar', - keyID: 'D523BD50DD70B0BA', - fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', - keygrip: '3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627' -}; +const userInfos = [ + { + key: 'test-key', + pgp: fs.readFileSync('__tests__/fixtures/test-key.pgp', { + encoding: 'utf8', + flag: 'r' + }), + pgp_base64: fs.readFileSync('__tests__/fixtures/test-key-base64.pgp', { + encoding: 'utf8', + flag: 'r' + }), + passphrase: fs.readFileSync('__tests__/fixtures/test-key.pass', { + encoding: 'utf8', + flag: 'r' + }), + name: 'Joe Tester', + email: 'joe@foo.bar', + keyID: '7D851EB72D73BDA0', + fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', + keygrip: '3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627' + }, + { + key: 'test-subkey', + pgp: fs.readFileSync('__tests__/fixtures/test-subkey.pgp', { + encoding: 'utf8', + flag: 'r' + }), + pgp_base64: fs.readFileSync('__tests__/fixtures/test-subkey-base64.pgp', { + encoding: 'utf8', + flag: 'r' + }), + passphrase: fs.readFileSync('__tests__/fixtures/test-subkey.pass', { + encoding: 'utf8', + flag: 'r' + }), + name: 'Joe Bar', + email: 'joe@bar.foo', + keyID: '6071D218380FDCC8', + fingerprint: '87F257B89CE462100BEC0FFE6071D218380FDCC8', + keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB'] + } +]; -describe('openpgp', () => { - describe('readPrivateKey', () => { - it('returns a PGP private key from an armored string', async () => { - await openpgp.readPrivateKey(userInfo.pgp).then(privateKey => { - expect(privateKey.keyID).toEqual(userInfo.keyID); - expect(privateKey.name).toEqual(userInfo.name); - expect(privateKey.email).toEqual(userInfo.email); - expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); +for (let userInfo of userInfos) { + describe(userInfo.key, () => { + describe('readPrivateKey', () => { + it('returns a PGP private key from an armored string', async () => { + await openpgp.readPrivateKey(userInfo.pgp).then(privateKey => { + expect(privateKey.keyID).toEqual(userInfo.keyID); + expect(privateKey.name).toEqual(userInfo.name); + expect(privateKey.email).toEqual(userInfo.email); + expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); + }); }); - }); - it('returns a PGP private key from a base64 armored string', async () => { - await openpgp.readPrivateKey(userInfo.pgp_base64).then(privateKey => { - expect(privateKey.keyID).toEqual(userInfo.keyID); - expect(privateKey.name).toEqual(userInfo.name); - expect(privateKey.email).toEqual(userInfo.email); - expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); + it('returns a PGP private key from a base64 armored string', async () => { + await openpgp.readPrivateKey(userInfo.pgp_base64).then(privateKey => { + expect(privateKey.keyID).toEqual(userInfo.keyID); + expect(privateKey.name).toEqual(userInfo.name); + expect(privateKey.email).toEqual(userInfo.email); + expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); + }); }); }); - }); - describe('generateKeyPair', () => { - it('generates a PGP key pair', async () => { - await openpgp.generateKeyPair(userInfo.name, userInfo.email, userInfo.passphrase).then(keyPair => { - expect(keyPair).not.toBeUndefined(); - expect(keyPair.publicKey).not.toBeUndefined(); - expect(keyPair.privateKey).not.toBeUndefined(); - }); - }, 30000); - }); + describe('generateKeyPair', () => { + it('generates a PGP key pair', async () => { + await openpgp.generateKeyPair(userInfo.name, userInfo.email, userInfo.passphrase).then(keyPair => { + expect(keyPair).not.toBeUndefined(); + expect(keyPair.publicKey).not.toBeUndefined(); + expect(keyPair.privateKey).not.toBeUndefined(); + }); + }, 30000); + }); - describe('isArmored', () => { - it('returns true for armored key string', async () => { - await openpgp.isArmored(userInfo.pgp).then(armored => { - expect(armored).toEqual(true); + describe('isArmored', () => { + it('returns true for armored key string', async () => { + await openpgp.isArmored(userInfo.pgp).then(armored => { + expect(armored).toEqual(true); + }); }); - }); - it('returns false for base64 key string', async () => { - await openpgp.isArmored(userInfo.pgp_base64).then(armored => { - expect(armored).toEqual(false); + it('returns false for base64 key string', async () => { + await openpgp.isArmored(userInfo.pgp_base64).then(armored => { + expect(armored).toEqual(false); + }); }); }); }); -}); +} diff --git a/action.yml b/action.yml index 452e6ddb..64d6d494 100644 --- a/action.yml +++ b/action.yml @@ -43,6 +43,9 @@ inputs: description: 'Working directory (below repository root)' default: '.' required: false + fingerprint: + description: 'Specific fingerprint to use (subkey)' + required: false outputs: fingerprint: diff --git a/dist/index.js b/dist/index.js index d6918de5..f7064209 100644 --- a/dist/index.js +++ b/dist/index.js @@ -51,7 +51,8 @@ function getInputs() { gitPushGpgsign: core.getInput('git_push_gpgsign') || 'if-asked', gitCommitterName: core.getInput('git_committer_name'), gitCommitterEmail: core.getInput('git_committer_email'), - workdir: core.getInput('workdir') || '.' + workdir: core.getInput('workdir') || '.', + fingerprint: core.getInput('fingerprint') }; }); } @@ -389,7 +390,6 @@ function run() { return __awaiter(this, void 0, void 0, function* () { try { let inputs = yield context.getInputs(); - stateHelper.setGpgPrivateKey(inputs.gpgPrivateKey); if (inputs.workdir && inputs.workdir !== '.') { core.info(`📂 Using ${inputs.workdir} as working directory...`); process.chdir(inputs.workdir); @@ -411,6 +411,14 @@ function run() { core.info(`Email : ${privateKey.email}`); core.info(`CreationTime : ${privateKey.creationTime}`); })); + let fingerprint = privateKey.fingerprint; + if (inputs.fingerprint) { + fingerprint = inputs.fingerprint; + } + stateHelper.setFingerprint(fingerprint); + yield core.group(`Fingerprint to use`, () => __awaiter(this, void 0, void 0, function* () { + core.info(fingerprint); + })); yield core.group(`Importing GPG private key`, () => __awaiter(this, void 0, void 0, function* () { yield gpg.importKey(inputs.gpgPrivateKey).then(stdout => { core.info(stdout); @@ -420,7 +428,7 @@ function run() { core.info('Configuring GnuPG agent'); yield gpg.configureAgent(gpg.agentConfig); yield core.group(`Getting keygrips`, () => __awaiter(this, void 0, void 0, function* () { - for (let keygrip of yield gpg.getKeygrips(privateKey.fingerprint)) { + for (let keygrip of yield gpg.getKeygrips(fingerprint)) { core.info(`Presetting passphrase for ${keygrip}`); yield gpg.presetPassphrase(keygrip, inputs.passphrase).then(stdout => { core.debug(stdout); @@ -428,11 +436,16 @@ function run() { } })); } - core.info('Setting outputs'); - context.setOutput('fingerprint', privateKey.fingerprint); - context.setOutput('keyid', privateKey.keyID); - context.setOutput('name', privateKey.name); - context.setOutput('email', privateKey.email); + yield core.group(`Setting outputs`, () => __awaiter(this, void 0, void 0, function* () { + core.info(`fingerprint=${fingerprint}`); + context.setOutput('fingerprint', fingerprint); + core.info(`keyid=${privateKey.keyID}`); + context.setOutput('keyid', privateKey.keyID); + core.info(`name=${privateKey.name}`); + context.setOutput('name', privateKey.name); + core.info(`email=${privateKey.email}`); + context.setOutput('email', privateKey.email); + })); if (inputs.gitUserSigningkey) { core.info('Setting GPG signing keyID for this Git repository'); yield git.setConfig('user.signingkey', privateKey.keyID, inputs.gitConfigGlobal); @@ -466,14 +479,13 @@ function run() { } function cleanup() { return __awaiter(this, void 0, void 0, function* () { - if (stateHelper.gpgPrivateKey.length <= 0) { - core.debug('GPG private key is not defined. Skipping cleanup.'); + if (stateHelper.fingerprint.length <= 0) { + core.debug('Fingerprint is not defined. Skipping cleanup.'); return; } try { core.info('Removing keys'); - const privateKey = yield openpgp.readPrivateKey(stateHelper.gpgPrivateKey); - yield gpg.deleteKey(privateKey.fingerprint); + yield gpg.deleteKey(stateHelper.fingerprint); core.info('Killing GnuPG agent'); yield gpg.killAgent(); } @@ -542,10 +554,7 @@ exports.readPrivateKey = (key) => __awaiter(void 0, void 0, void 0, function* () }); return { fingerprint: privateKey.getFingerprint().toUpperCase(), - keyID: yield privateKey.getEncryptionKey().then(encKey => { - // @ts-ignore - return encKey === null || encKey === void 0 ? void 0 : encKey.getKeyID().toHex().toUpperCase(); - }), + keyID: privateKey.getKeyID().toHex().toUpperCase(), name: address.name, email: address.address, creationTime: privateKey.getCreationTime() @@ -594,14 +603,14 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.setGpgPrivateKey = exports.gpgPrivateKey = exports.IsPost = void 0; +exports.setFingerprint = exports.fingerprint = exports.IsPost = void 0; const core = __importStar(__webpack_require__(186)); exports.IsPost = !!process.env['STATE_isPost']; -exports.gpgPrivateKey = process.env['STATE_gpgPrivateKey'] || ''; -function setGpgPrivateKey(gpgPrivateKey) { - core.saveState('gpgPrivateKey', gpgPrivateKey); +exports.fingerprint = process.env['STATE_fingerprint'] || ''; +function setFingerprint(fingerprint) { + core.saveState('fingerprint', fingerprint); } -exports.setGpgPrivateKey = setGpgPrivateKey; +exports.setFingerprint = setFingerprint; if (!exports.IsPost) { core.saveState('isPost', 'true'); } diff --git a/src/context.ts b/src/context.ts index f7ad8842..57f5e78f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -12,6 +12,7 @@ export interface Inputs { gitCommitterName: string; gitCommitterEmail: string; workdir: string; + fingerprint: string; } export async function getInputs(): Promise { @@ -25,7 +26,8 @@ export async function getInputs(): Promise { gitPushGpgsign: core.getInput('git_push_gpgsign') || 'if-asked', gitCommitterName: core.getInput('git_committer_name'), gitCommitterEmail: core.getInput('git_committer_email'), - workdir: core.getInput('workdir') || '.' + workdir: core.getInput('workdir') || '.', + fingerprint: core.getInput('fingerprint') }; } diff --git a/src/main.ts b/src/main.ts index 2ac675cb..4ecf9717 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,6 @@ import * as stateHelper from './state-helper'; async function run(): Promise { try { let inputs: context.Inputs = await context.getInputs(); - stateHelper.setGpgPrivateKey(inputs.gpgPrivateKey); if (inputs.workdir && inputs.workdir !== '.') { core.info(`📂 Using ${inputs.workdir} as working directory...`); @@ -34,6 +33,15 @@ async function run(): Promise { core.info(`CreationTime : ${privateKey.creationTime}`); }); + let fingerprint = privateKey.fingerprint; + if (inputs.fingerprint) { + fingerprint = inputs.fingerprint; + } + stateHelper.setFingerprint(fingerprint); + await core.group(`Fingerprint to use`, async () => { + core.info(fingerprint); + }); + await core.group(`Importing GPG private key`, async () => { await gpg.importKey(inputs.gpgPrivateKey).then(stdout => { core.info(stdout); @@ -45,7 +53,7 @@ async function run(): Promise { await gpg.configureAgent(gpg.agentConfig); await core.group(`Getting keygrips`, async () => { - for (let keygrip of await gpg.getKeygrips(privateKey.fingerprint)) { + for (let keygrip of await gpg.getKeygrips(fingerprint)) { core.info(`Presetting passphrase for ${keygrip}`); await gpg.presetPassphrase(keygrip, inputs.passphrase).then(stdout => { core.debug(stdout); @@ -54,11 +62,16 @@ async function run(): Promise { }); } - core.info('Setting outputs'); - context.setOutput('fingerprint', privateKey.fingerprint); - context.setOutput('keyid', privateKey.keyID); - context.setOutput('name', privateKey.name); - context.setOutput('email', privateKey.email); + await core.group(`Setting outputs`, async () => { + core.info(`fingerprint=${fingerprint}`); + context.setOutput('fingerprint', fingerprint); + core.info(`keyid=${privateKey.keyID}`); + context.setOutput('keyid', privateKey.keyID); + core.info(`name=${privateKey.name}`); + context.setOutput('name', privateKey.name); + core.info(`email=${privateKey.email}`); + context.setOutput('email', privateKey.email); + }); if (inputs.gitUserSigningkey) { core.info('Setting GPG signing keyID for this Git repository'); @@ -95,14 +108,13 @@ async function run(): Promise { } async function cleanup(): Promise { - if (stateHelper.gpgPrivateKey.length <= 0) { - core.debug('GPG private key is not defined. Skipping cleanup.'); + if (stateHelper.fingerprint.length <= 0) { + core.debug('Fingerprint is not defined. Skipping cleanup.'); return; } try { core.info('Removing keys'); - const privateKey = await openpgp.readPrivateKey(stateHelper.gpgPrivateKey); - await gpg.deleteKey(privateKey.fingerprint); + await gpg.deleteKey(stateHelper.fingerprint); core.info('Killing GnuPG agent'); await gpg.killAgent(); diff --git a/src/openpgp.ts b/src/openpgp.ts index 9d0c87a9..91908efd 100644 --- a/src/openpgp.ts +++ b/src/openpgp.ts @@ -25,10 +25,7 @@ export const readPrivateKey = async (key: string): Promise => { return { fingerprint: privateKey.getFingerprint().toUpperCase(), - keyID: await privateKey.getEncryptionKey().then(encKey => { - // @ts-ignore - return encKey?.getKeyID().toHex().toUpperCase(); - }), + keyID: privateKey.getKeyID().toHex().toUpperCase(), name: address.name, email: address.address, creationTime: privateKey.getCreationTime() diff --git a/src/state-helper.ts b/src/state-helper.ts index c364a1ed..09b43374 100644 --- a/src/state-helper.ts +++ b/src/state-helper.ts @@ -1,10 +1,10 @@ import * as core from '@actions/core'; export const IsPost = !!process.env['STATE_isPost']; -export const gpgPrivateKey = process.env['STATE_gpgPrivateKey'] || ''; +export const fingerprint = process.env['STATE_fingerprint'] || ''; -export function setGpgPrivateKey(gpgPrivateKey: string) { - core.saveState('gpgPrivateKey', gpgPrivateKey); +export function setFingerprint(fingerprint: string) { + core.saveState('fingerprint', fingerprint); } if (!IsPost) {