Allow using any user id of the key's user ids as committer identity. Fixes #156

This commit is contained in:
Florian Friedrich 2024-04-29 11:11:31 +02:00
parent 78fb6ec0e4
commit 5170336089
No known key found for this signature in database
GPG key ID: 5F2F8CADF15B7C89
5 changed files with 73 additions and 33 deletions

View file

@ -230,11 +230,12 @@ The following inputs can be used as `step.with` keys
Following outputs are available Following outputs are available
| Name | Type | Description | | Name | Type | Description |
|---------------|--------|---------------------------------------------------------------------------------------------------------------------------------| |---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fingerprint` | String | Fingerprint of the GPG key (recommended as [user ID](https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html)) | | `fingerprint` | String | Fingerprint of the GPG key (recommended as [user ID](https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html)) |
| `keyid` | String | Low 64 bits of the X.509 certificate SHA-1 fingerprint | | `keyid` | String | Low 64 bits of the X.509 certificate SHA-1 fingerprint |
| `name` | String | Name associated with the GPG key | | `name` | String | Primary name associated with the GPG key |
| `email` | String | Email address associated with the GPG key | | `email` | String | Primary email address associated with the GPG key |
| `userids` | String (JSON) | All user ids (including primary) associated with the GPG Key.<br/>The output is a JSON array where each object has a `name` and `email` key. Use [fromJson](https://docs.github.com/en/actions/learn-github-actions/expressions#fromjson) to turn the String back into a JSON array |
## Contributing ## Contributing

View file

@ -17,8 +17,16 @@ const userInfos = [
encoding: 'utf8', encoding: 'utf8',
flag: 'r' flag: 'r'
}), }),
primaryUserId: {
name: 'Joe Tester', name: 'Joe Tester',
email: 'joe@foo.bar', email: 'joe@foo.bar'
},
userIds: [
{
name: 'Joe Tester',
email: 'joe@foo.bar'
}
],
keyID: '7D851EB72D73BDA0', keyID: '7D851EB72D73BDA0',
fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0',
keygrip: '3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627' keygrip: '3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627'
@ -37,8 +45,16 @@ const userInfos = [
encoding: 'utf8', encoding: 'utf8',
flag: 'r' flag: 'r'
}), }),
primaryUserId: {
name: 'Joe Bar', name: 'Joe Bar',
email: 'joe@bar.foo', email: 'joe@bar.foo'
},
userIds: [
{
name: 'Joe Bar',
email: 'joe@bar.foo'
}
],
keyID: '6071D218380FDCC8', keyID: '6071D218380FDCC8',
fingerprint: '87F257B89CE462100BEC0FFE6071D218380FDCC8', fingerprint: '87F257B89CE462100BEC0FFE6071D218380FDCC8',
keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB'] keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB']
@ -52,16 +68,22 @@ for (const userInfo of userInfos) {
it('returns a PGP private key from an armored string', async () => { it('returns a PGP private key from an armored string', async () => {
await openpgp.readPrivateKey(userInfo.pgp).then(privateKey => { await openpgp.readPrivateKey(userInfo.pgp).then(privateKey => {
expect(privateKey.keyID).toEqual(userInfo.keyID); expect(privateKey.keyID).toEqual(userInfo.keyID);
expect(privateKey.name).toEqual(userInfo.name); expect(privateKey.primaryUserId.name).toEqual(userInfo.primaryUserId.name);
expect(privateKey.email).toEqual(userInfo.email); expect(privateKey.primaryUserId.email).toEqual(userInfo.primaryUserId.email);
expect(privateKey.allUserIds).toHaveLength(userInfo.userIds.length);
expect(privateKey.allUserIds[0].name).toEqual(userInfo.userIds[0].name);
expect(privateKey.allUserIds[0].email).toEqual(userInfo.userIds[0].email);
expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); expect(privateKey.fingerprint).toEqual(userInfo.fingerprint);
}); });
}); });
it('returns a PGP private key from a base64 armored string', async () => { it('returns a PGP private key from a base64 armored string', async () => {
await openpgp.readPrivateKey(userInfo.pgp_base64).then(privateKey => { await openpgp.readPrivateKey(userInfo.pgp_base64).then(privateKey => {
expect(privateKey.keyID).toEqual(userInfo.keyID); expect(privateKey.keyID).toEqual(userInfo.keyID);
expect(privateKey.name).toEqual(userInfo.name); expect(privateKey.primaryUserId.name).toEqual(userInfo.primaryUserId.name);
expect(privateKey.email).toEqual(userInfo.email); expect(privateKey.primaryUserId.email).toEqual(userInfo.primaryUserId.email);
expect(privateKey.allUserIds).toHaveLength(userInfo.userIds.length);
expect(privateKey.allUserIds[0].name).toEqual(userInfo.userIds[0].name);
expect(privateKey.allUserIds[0].email).toEqual(userInfo.userIds[0].email);
expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); expect(privateKey.fingerprint).toEqual(userInfo.fingerprint);
}); });
}); });
@ -69,7 +91,7 @@ for (const userInfo of userInfos) {
describe('generateKeyPair', () => { describe('generateKeyPair', () => {
it('generates a PGP key pair', async () => { it('generates a PGP key pair', async () => {
await openpgp.generateKeyPair(userInfo.name, userInfo.email, userInfo.passphrase).then(keyPair => { await openpgp.generateKeyPair(userInfo.primaryUserId.name, userInfo.primaryUserId.email, userInfo.passphrase).then(keyPair => {
expect(keyPair).not.toBeUndefined(); expect(keyPair).not.toBeUndefined();
expect(keyPair.publicKey).not.toBeUndefined(); expect(keyPair.publicKey).not.toBeUndefined();
expect(keyPair.privateKey).not.toBeUndefined(); expect(keyPair.privateKey).not.toBeUndefined();

View file

@ -56,9 +56,11 @@ outputs:
keyid: keyid:
description: 'Low 64 bits of the X.509 certificate SHA-1 fingerprint' description: 'Low 64 bits of the X.509 certificate SHA-1 fingerprint'
name: name:
description: 'Name associated with the GPG key' description: 'Primary name associated with the GPG key'
email: email:
description: 'Email address associated with the GPG key' description: 'Primary email address associated with the GPG key'
userids:
description: 'All user ids (including primary) associated with the GPG Key'
runs: runs:
using: 'node20' using: 'node20'

View file

@ -28,8 +28,10 @@ async function run(): Promise<void> {
await core.group(`GPG private key info`, async () => { await core.group(`GPG private key info`, async () => {
core.info(`Fingerprint : ${privateKey.fingerprint}`); core.info(`Fingerprint : ${privateKey.fingerprint}`);
core.info(`KeyID : ${privateKey.keyID}`); core.info(`KeyID : ${privateKey.keyID}`);
core.info(`Name : ${privateKey.name}`); for (const userId of privateKey.allUserIds) {
core.info(`Email : ${privateKey.email}`); const isPrimary = userId.email === privateKey.primaryUserId.email;
core.info(`User ID : ${userId.name} <${userId.email}>${isPrimary ? ' (primary)' : ''}`);
}
core.info(`CreationTime : ${privateKey.creationTime}`); core.info(`CreationTime : ${privateKey.creationTime}`);
}); });
@ -91,21 +93,24 @@ async function run(): Promise<void> {
core.setOutput('fingerprint', fingerprint); core.setOutput('fingerprint', fingerprint);
core.info(`keyid=${privateKey.keyID}`); core.info(`keyid=${privateKey.keyID}`);
core.setOutput('keyid', privateKey.keyID); core.setOutput('keyid', privateKey.keyID);
core.info(`name=${privateKey.name}`); core.info(`name=${privateKey.primaryUserId.name}`);
core.setOutput('name', privateKey.name); core.setOutput('name', privateKey.primaryUserId.name);
core.info(`email=${privateKey.email}`); core.info(`email=${privateKey.primaryUserId.email}`);
core.setOutput('email', privateKey.email); core.setOutput('email', privateKey.primaryUserId.email);
core.info(`userids=${JSON.stringify(privateKey.allUserIds)}`);
core.setOutput('userids', privateKey.allUserIds);
}); });
if (inputs.gitUserSigningkey) { if (inputs.gitUserSigningkey) {
core.info('Setting GPG signing keyID for this Git repository'); core.info('Setting GPG signing keyID for this Git repository');
await git.setConfig('user.signingkey', privateKey.keyID, inputs.gitConfigGlobal); await git.setConfig('user.signingkey', privateKey.keyID, inputs.gitConfigGlobal);
const userEmail = inputs.gitCommitterEmail || privateKey.email; const userName = inputs.gitCommitterName || privateKey.primaryUserId.name;
const userName = inputs.gitCommitterName || privateKey.name; const userEmail = inputs.gitCommitterEmail || privateKey.primaryUserId.email;
if (userEmail != privateKey.email) { if (!privateKey.allUserIds.some(id => id.email === userEmail)) {
core.setFailed(`Committer email "${inputs.gitCommitterEmail}" (name: "${inputs.gitCommitterName}") does not match GPG private key email "${privateKey.email}" (name: "${privateKey.name}")`); const keyIdentities = privateKey.allUserIds.map(id => `"${id.email}" (name: "${id.name}")`).join(', ');
core.setFailed(`Committer email "${inputs.gitCommitterEmail}" (name: "${inputs.gitCommitterName}") does not match GPG any of the private key user id email addresses: ${keyIdentities}`);
return; return;
} }

View file

@ -1,11 +1,16 @@
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
import addressparser from 'addressparser'; import addressparser from 'addressparser';
export interface UserId {
name: string;
email: string;
}
export interface PrivateKey { export interface PrivateKey {
fingerprint: string; fingerprint: string;
keyID: string; keyID: string;
name: string; primaryUserId: UserId;
email: string; allUserIds: UserId[];
creationTime: Date; creationTime: Date;
} }
@ -19,15 +24,20 @@ export const readPrivateKey = async (key: string): Promise<PrivateKey> => {
armoredKey: (await isArmored(key)) ? key : Buffer.from(key, 'base64').toString() armoredKey: (await isArmored(key)) ? key : Buffer.from(key, 'base64').toString()
}); });
const address = await privateKey.getPrimaryUser().then(primaryUser => { const primaryUserId: UserId = await privateKey.getPrimaryUser().then(primaryUser => {
return addressparser(primaryUser.user.userID?.userID)[0]; const address = addressparser(primaryUser.user.userID?.userID)[0];
return {name: address.name, email: address.address};
});
const allUserIds: UserId[] = privateKey.getUserIDs().map(userId => {
const address = addressparser(userId)[0];
return {name: address.name, email: address.address};
}); });
return { return {
fingerprint: privateKey.getFingerprint().toUpperCase(), fingerprint: privateKey.getFingerprint().toUpperCase(),
keyID: privateKey.getKeyID().toHex().toUpperCase(), keyID: privateKey.getKeyID().toHex().toUpperCase(),
name: address.name, primaryUserId: primaryUserId,
email: address.address, allUserIds: allUserIds,
creationTime: privateKey.getCreationTime() creationTime: privateKey.getCreationTime()
}; };
}; };