mirror of
https://github.com/crazy-max/ghaction-import-gpg.git
synced 2024-11-25 14:31:03 -05:00
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:
parent
343bb932e5
commit
2724049ae2
7 changed files with 197 additions and 3 deletions
45
README.md
45
README.md
|
@ -120,6 +120,51 @@ jobs:
|
||||||
git push
|
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
|
## Customizing
|
||||||
|
|
||||||
### inputs
|
### inputs
|
||||||
|
|
8
__tests__/fixtures/test-key-gpg-output.txt
Normal file
8
__tests__/fixtures/test-key-gpg-output.txt
Normal 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:
|
8
__tests__/fixtures/test-subkey-gpg-output.txt
Normal file
8
__tests__/fixtures/test-subkey-gpg-output.txt
Normal 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:
|
|
@ -1,5 +1,6 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as gpg from '../src/gpg';
|
import * as gpg from '../src/gpg';
|
||||||
|
import {parseKeygripFromGpgColonsOutput} from '../src/gpg';
|
||||||
|
|
||||||
const userInfos = [
|
const userInfos = [
|
||||||
{
|
{
|
||||||
|
@ -20,6 +21,7 @@ const userInfos = [
|
||||||
email: 'joe@foo.bar',
|
email: 'joe@foo.bar',
|
||||||
keyID: '7D851EB72D73BDA0',
|
keyID: '7D851EB72D73BDA0',
|
||||||
fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0',
|
fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0',
|
||||||
|
fingerprints: ['27571A53B86AF0C799B38BA77D851EB72D73BDA0', '5A282E1460C0BC419615D34DD523BD50DD70B0BA'],
|
||||||
keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163']
|
keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -40,6 +42,7 @@ const userInfos = [
|
||||||
email: 'joe@bar.foo',
|
email: 'joe@bar.foo',
|
||||||
keyID: '6071D218380FDCC8',
|
keyID: '6071D218380FDCC8',
|
||||||
fingerprint: 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8',
|
fingerprint: 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8',
|
||||||
|
fingerprints: ['87F257B89CE462100BEC0FFE6071D218380FDCC8', 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8'],
|
||||||
keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB']
|
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', () => {
|
describe('presetPassphrase', () => {
|
||||||
it('presets passphrase', async () => {
|
it('presets passphrase', async () => {
|
||||||
await gpg.importKey(userInfo.pgp);
|
await gpg.importKey(userInfo.pgp);
|
||||||
|
@ -128,3 +144,29 @@ describe('killAgent', () => {
|
||||||
await gpg.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
45
dist/index.js
generated
vendored
|
@ -164,7 +164,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
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 exec = __importStar(__webpack_require__(1514));
|
||||||
const fs = __importStar(__webpack_require__(5747));
|
const fs = __importStar(__webpack_require__(5747));
|
||||||
const path = __importStar(__webpack_require__(5622));
|
const path = __importStar(__webpack_require__(5622));
|
||||||
|
@ -304,6 +304,34 @@ exports.getKeygrips = (fingerprint) => __awaiter(void 0, void 0, void 0, functio
|
||||||
return keygrips;
|
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* () {
|
exports.configureAgent = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
const gpgAgentConf = path.join(yield getGnupgHome(), 'gpg-agent.conf');
|
const gpgAgentConf = path.join(yield getGnupgHome(), 'gpg-agent.conf');
|
||||||
yield fs.writeFile(gpgAgentConf, config, function (err) {
|
yield fs.writeFile(gpgAgentConf, config, function (err) {
|
||||||
|
@ -424,7 +452,8 @@ function run() {
|
||||||
core.info(stdout);
|
core.info(stdout);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
if (inputs.passphrase) {
|
if (inputs.passphrase && !inputs.fingerprint) {
|
||||||
|
// Set the passphrase for all subkeys
|
||||||
core.info('Configuring GnuPG agent');
|
core.info('Configuring GnuPG agent');
|
||||||
yield gpg.configureAgent(gpg.agentConfig);
|
yield gpg.configureAgent(gpg.agentConfig);
|
||||||
yield core.group(`Getting keygrips`, () => __awaiter(this, void 0, void 0, function* () {
|
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* () {
|
yield core.group(`Setting outputs`, () => __awaiter(this, void 0, void 0, function* () {
|
||||||
core.info(`fingerprint=${fingerprint}`);
|
core.info(`fingerprint=${fingerprint}`);
|
||||||
context.setOutput('fingerprint', fingerprint);
|
context.setOutput('fingerprint', fingerprint);
|
||||||
|
|
33
src/gpg.ts
33
src/gpg.ts
|
@ -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> => {
|
export const configureAgent = async (config: string): Promise<void> => {
|
||||||
const gpgAgentConf = path.join(await getGnupgHome(), 'gpg-agent.conf');
|
const gpgAgentConf = path.join(await getGnupgHome(), 'gpg-agent.conf');
|
||||||
await fs.writeFile(gpgAgentConf, config, function (err) {
|
await fs.writeFile(gpgAgentConf, config, function (err) {
|
||||||
|
|
19
src/main.ts
19
src/main.ts
|
@ -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');
|
core.info('Configuring GnuPG agent');
|
||||||
await gpg.configureAgent(gpg.agentConfig);
|
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 () => {
|
await core.group(`Setting outputs`, async () => {
|
||||||
core.info(`fingerprint=${fingerprint}`);
|
core.info(`fingerprint=${fingerprint}`);
|
||||||
context.setOutput('fingerprint', fingerprint);
|
context.setOutput('fingerprint', fingerprint);
|
||||||
|
|
Loading…
Reference in a new issue