Skip to content

Commit

Permalink
Set passphrase only for the fingerprint being used (#123)
Browse files Browse the repository at this point in the history
* If fingerprint input is provided it sets only the passphrase for that key

* Update README with how to use subkeys example
  • Loading branch information
josecelano authored Feb 28, 2022
1 parent 343bb93 commit 2724049
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 3 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,51 @@ jobs:
git push
```
### Use a subkey
With the input `fingerprint`, you can specify which one of the subkeys in a GPG key you want to use for signing.

```yaml
name: import-gpg
on:
push:
branches: master
jobs:
import-gpg:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v4
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
fingerprint: "C17D11ADF199F12A30A0910F1F80449BE0B08CB8"
-
name: List keys
run: gpg -K
```

For example, given this GPG key with a signing subkey:

```s
pub ed25519 2021-09-24 [C]
87F257B89CE462100BEC0FFE6071D218380FDCC8
Keygrip = F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092
uid [ unknown] Joe Bar <joe@bar.foo>
sub ed25519 2021-09-24 [S]
C17D11ADF199F12A30A0910F1F80449BE0B08CB8
Keygrip = DEE0FC98F441519CA5DE5D79773CB29009695FEB
```

You can use the subkey with signing capability whose fingerprint is `C17D11ADF199F12A30A0910F1F80449BE0B08CB8`.

## Customizing

### inputs
Expand Down
8 changes: 8 additions & 0 deletions __tests__/fixtures/test-key-gpg-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tru::1:1645715610:1661267528:3:1:5
pub:-:4096:1:7D851EB72D73BDA0:1588448672:::-:::scESC::::::23::0:
fpr:::::::::27571A53B86AF0C799B38BA77D851EB72D73BDA0:
grp:::::::::3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627:
uid:-::::1588448672::C1B25336F8F0F0F22BAF57137BE493ADEDA8CCAA::Joe Tester <joe@foo.bar>::::::::::0:
sub:-:4096:1:D523BD50DD70B0BA:1588448672::::::e::::::23:
fpr:::::::::5A282E1460C0BC419615D34DD523BD50DD70B0BA:
grp:::::::::BA83FC8947213477F28ADC019F6564A956456163:
8 changes: 8 additions & 0 deletions __tests__/fixtures/test-subkey-gpg-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tru::1:1645715610:1661267528:3:1:5
pub:-:256:22:6071D218380FDCC8:1632521434:::-:::cSC:::::ed25519:::0:
fpr:::::::::87F257B89CE462100BEC0FFE6071D218380FDCC8:
grp:::::::::F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092:
uid:-::::1632521434::019F22ECD701BC0F6AFE686ABD2B010B812B828E::Joe Bar <joe@bar.foo>::::::::::0:
sub:-:256:22:1F80449BE0B08CB8:1632521539::::::s:::::ed25519::
fpr:::::::::C17D11ADF199F12A30A0910F1F80449BE0B08CB8:
grp:::::::::DEE0FC98F441519CA5DE5D79773CB29009695FEB:
42 changes: 42 additions & 0 deletions __tests__/gpg.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as gpg from '../src/gpg';
import {parseKeygripFromGpgColonsOutput} from '../src/gpg';

const userInfos = [
{
Expand All @@ -20,6 +21,7 @@ const userInfos = [
email: 'joe@foo.bar',
keyID: '7D851EB72D73BDA0',
fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0',
fingerprints: ['27571A53B86AF0C799B38BA77D851EB72D73BDA0', '5A282E1460C0BC419615D34DD523BD50DD70B0BA'],
keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163']
},
{
Expand All @@ -40,6 +42,7 @@ const userInfos = [
email: 'joe@bar.foo',
keyID: '6071D218380FDCC8',
fingerprint: 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8',
fingerprints: ['87F257B89CE462100BEC0FFE6071D218380FDCC8', 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8'],
keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB']
}
];
Expand Down Expand Up @@ -101,6 +104,19 @@ for (let userInfo of userInfos) {
});
});

describe('getKeygrip', () => {
it('returns the keygrip for a given fingerprint', async () => {
await gpg.importKey(userInfo.pgp);
for (let [i, fingerprint] of userInfo.fingerprints.entries()) {
await gpg.getKeygrip(fingerprint).then(keygrip => {
console.log(`Fingerprint: ${fingerprint}; Index: ${i}; Keygrip: ${keygrip}`);
expect(keygrip.length).toEqual(userInfo.keygrips[i].length);
expect(keygrip).toEqual(userInfo.keygrips[i]);
});
}
});
});

describe('presetPassphrase', () => {
it('presets passphrase', async () => {
await gpg.importKey(userInfo.pgp);
Expand Down Expand Up @@ -128,3 +144,29 @@ describe('killAgent', () => {
await gpg.killAgent();
});
});

describe('parseKeygripFromGpgColonsOutput', () => {
it('returns the keygrip of a given fingerprint from a GPG command output using the option: --with-colons', async () => {
const outputUsingTestKey = fs.readFileSync('__tests__/fixtures/test-key-gpg-output.txt', {
encoding: 'utf8',
flag: 'r'
});

const keygripPrimaryTestKey = parseKeygripFromGpgColonsOutput(outputUsingTestKey, '27571A53B86AF0C799B38BA77D851EB72D73BDA0');
expect(keygripPrimaryTestKey).toBe('3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627');

const keygripSubkeyTestKey = parseKeygripFromGpgColonsOutput(outputUsingTestKey, '5A282E1460C0BC419615D34DD523BD50DD70B0BA');
expect(keygripSubkeyTestKey).toBe('BA83FC8947213477F28ADC019F6564A956456163');

const outputUsingTestSubkey = fs.readFileSync('__tests__/fixtures/test-subkey-gpg-output.txt', {
encoding: 'utf8',
flag: 'r'
});

const keygripPrimaryTestSubkey = parseKeygripFromGpgColonsOutput(outputUsingTestSubkey, '87F257B89CE462100BEC0FFE6071D218380FDCC8');
expect(keygripPrimaryTestSubkey).toBe('F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092');

const keygripSubkeyTestSubkey = parseKeygripFromGpgColonsOutput(outputUsingTestSubkey, 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8');
expect(keygripSubkeyTestSubkey).toBe('DEE0FC98F441519CA5DE5D79773CB29009695FEB');
});
});
45 changes: 43 additions & 2 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions src/gpg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,39 @@ export const getKeygrips = async (fingerprint: string): Promise<Array<string>> =
});
};

export const parseKeygripFromGpgColonsOutput = (output: string, fingerprint: string): string => {
let keygrip = '';
let fingerPrintFound = false;
const lines = output.replace(/\r/g, '').trim().split(/\n/g);

for (let line of lines) {
if (line.startsWith(`fpr:`) && line.includes(`:${fingerprint}:`)) {
// We reach the record with the matching fingerprint.
// The next keygrip record is the keygrip for this fingerprint.
fingerPrintFound = true;
continue;
}

if (line.startsWith('grp:') && fingerPrintFound) {
keygrip = line.replace(/(grp|:)/g, '').trim();
break;
}
}

return keygrip;
};

export const getKeygrip = async (fingerprint: string): Promise<string> => {
return await exec
.getExecOutput('gpg', ['--batch', '--with-colons', '--with-keygrip', '--list-secret-keys', fingerprint], {
ignoreReturnCode: true,
silent: true
})
.then(res => {
return parseKeygripFromGpgColonsOutput(res.stdout, fingerprint);
});
};

export const configureAgent = async (config: string): Promise<void> => {
const gpgAgentConf = path.join(await getGnupgHome(), 'gpg-agent.conf');
await fs.writeFile(gpgAgentConf, config, function (err) {
Expand Down
19 changes: 18 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ async function run(): Promise<void> {
});
});

if (inputs.passphrase) {
if (inputs.passphrase && !inputs.fingerprint) {
// Set the passphrase for all subkeys

core.info('Configuring GnuPG agent');
await gpg.configureAgent(gpg.agentConfig);

Expand All @@ -62,6 +64,21 @@ async function run(): Promise<void> {
});
}

if (inputs.passphrase && inputs.fingerprint) {
// Set the passphrase only for the subkey specified in the input `fingerprint`

core.info('Configuring GnuPG agent');
await gpg.configureAgent(gpg.agentConfig);

await core.group(`Getting keygrip for fingerprint`, async () => {
const keygrip = await gpg.getKeygrip(fingerprint);
core.info(`Presetting passphrase for key ${fingerprint} with keygrip ${keygrip}`);
await gpg.presetPassphrase(keygrip, inputs.passphrase).then(stdout => {
core.debug(stdout);
});
});
}

await core.group(`Setting outputs`, async () => {
core.info(`fingerprint=${fingerprint}`);
context.setOutput('fingerprint', fingerprint);
Expand Down

0 comments on commit 2724049

Please sign in to comment.