Set passphrase only for the fingerprint being used (#123)

* If fingerprint input is provided it sets only the passphrase for that key

* Update README with how to use subkeys example
This commit is contained in:
Jose Celano 2022-02-28 15:36:54 +00:00 committed by GitHub
parent 343bb932e5
commit 2724049ae2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 197 additions and 3 deletions

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as gpg from '../src/gpg';
import {parseKeygripFromGpgColonsOutput} from '../src/gpg';
const userInfos = [
{
@ -20,6 +21,7 @@ const userInfos = [
email: 'joe@foo.bar',
keyID: '7D851EB72D73BDA0',
fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0',
fingerprints: ['27571A53B86AF0C799B38BA77D851EB72D73BDA0', '5A282E1460C0BC419615D34DD523BD50DD70B0BA'],
keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163']
},
{
@ -40,6 +42,7 @@ const userInfos = [
email: 'joe@bar.foo',
keyID: '6071D218380FDCC8',
fingerprint: 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8',
fingerprints: ['87F257B89CE462100BEC0FFE6071D218380FDCC8', 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8'],
keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB']
}
];
@ -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);
@ -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
dist/index.js generated vendored
View file

@ -164,7 +164,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.killAgent = exports.deleteKey = exports.presetPassphrase = exports.configureAgent = exports.getKeygrips = exports.importKey = exports.getDirs = exports.getVersion = exports.agentConfig = void 0;
exports.killAgent = exports.deleteKey = exports.presetPassphrase = exports.configureAgent = exports.getKeygrip = exports.parseKeygripFromGpgColonsOutput = exports.getKeygrips = exports.importKey = exports.getDirs = exports.getVersion = exports.agentConfig = void 0;
const exec = __importStar(__webpack_require__(1514));
const fs = __importStar(__webpack_require__(5747));
const path = __importStar(__webpack_require__(5622));
@ -304,6 +304,34 @@ exports.getKeygrips = (fingerprint) => __awaiter(void 0, void 0, void 0, functio
return keygrips;
});
});
exports.parseKeygripFromGpgColonsOutput = (output, fingerprint) => {
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;
};
exports.getKeygrip = (fingerprint) => __awaiter(void 0, void 0, void 0, function* () {
return yield exec
.getExecOutput('gpg', ['--batch', '--with-colons', '--with-keygrip', '--list-secret-keys', fingerprint], {
ignoreReturnCode: true,
silent: true
})
.then(res => {
return exports.parseKeygripFromGpgColonsOutput(res.stdout, fingerprint);
});
});
exports.configureAgent = (config) => __awaiter(void 0, void 0, void 0, function* () {
const gpgAgentConf = path.join(yield getGnupgHome(), 'gpg-agent.conf');
yield fs.writeFile(gpgAgentConf, config, function (err) {
@ -424,7 +452,8 @@ function run() {
core.info(stdout);
});
}));
if (inputs.passphrase) {
if (inputs.passphrase && !inputs.fingerprint) {
// Set the passphrase for all subkeys
core.info('Configuring GnuPG agent');
yield gpg.configureAgent(gpg.agentConfig);
yield core.group(`Getting keygrips`, () => __awaiter(this, void 0, void 0, function* () {
@ -436,6 +465,18 @@ function run() {
}
}));
}
if (inputs.passphrase && inputs.fingerprint) {
// Set the passphrase only for the subkey specified in the input `fingerprint`
core.info('Configuring GnuPG agent');
yield gpg.configureAgent(gpg.agentConfig);
yield core.group(`Getting keygrip for fingerprint`, () => __awaiter(this, void 0, void 0, function* () {
const keygrip = yield gpg.getKeygrip(fingerprint);
core.info(`Presetting passphrase for key ${fingerprint} with keygrip ${keygrip}`);
yield gpg.presetPassphrase(keygrip, inputs.passphrase).then(stdout => {
core.debug(stdout);
});
}));
}
yield core.group(`Setting outputs`, () => __awaiter(this, void 0, void 0, function* () {
core.info(`fingerprint=${fingerprint}`);
context.setOutput('fingerprint', fingerprint);

View file

@ -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) {

View file

@ -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);
@ -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);