Add GPG Signature option
The GitHub commit API allows signing a commit, but the signature must be calculated externally.
https://docs.github.com/en/rest/reference/git#create-a-commit
Is there a reason this PR is still open, isn't this now supported? @chingor13
Hi @IchordeDionysos,
Could you tell how to configure release-please to sign commits?
@Zebradil I'm testing around myself!
As I understand it's not possible right now to sign commits using release-please it would have to supply a commit signer.
My current status:
- I'm using the
openpgplibrary to sign commits. - I can construct most of the commit payload required for signing
- The only missing piece in the
code-suggesterlibrary is the commit author/committed date to properly sign the commit
I'm attaching my WIP code for signing the commit:
WIP commit signing
// These would be constructed/passed by release-please.
const octokit = new Octokit();
const fileData = new FileData('hello world', '100644');
const changes = new Map();
changes.set('foo.md', fileData);
// The PR would be created by release-please according to the release-please configuration.
await suggester.createPullRequest(octokit, changes, {
upstreamOwner: '<owner>',
upstreamRepo: '<repo>',
title: 'Test PR',
message: 'Test commit',
description: '',
fork: false,
force: true,
author: {
name: '<authorName>',
email: '<authorEmail>,
},
committer: {
name: '<committerName>',
email: '<committerEmail>',
},
// The next part would have to be added to release-please
signer: {
async generateSignature(commit: CommitData) {
// We'd need to figure out how to pass the private key and an optional passphrase for signing.
const privateKey = await openpgp.readPrivateKey({
armoredKey: privateKeyArmored,
});
// Here the commit/authored date is missing, otherwise, the payload looks good.
const commitObject = `tree ${commit.tree}
${commit.parents.map(parent => `parent ${parent}`).join('\n')}
author ${commit.author?.name ?? ''} <${commit.author?.email ?? ''}>
committer ${commit.committer?.name ?? ''} <${commit.committer?.email ?? ''}>
${commit.message}`;
console.log(commit);
console.log(commitObject);
const message = await openpgp.createCleartextMessage({
text: commitObject,
});
const signedMessage = await openpgp.sign({
message,
signingKeys: privateKey,
});
// We maybe need to find a more elegant solution to get only the signature (the `signedMessage` contains the passed message and only at the end the signature is attached)
const signature =
'-----BEGIN PGP SIGNATURE-----' +
signedMessage.split('-----BEGIN PGP SIGNATURE-----')[1];
console.log('signature', signature);
return signature;
},
},
});
Next steps to support code signing properly:
- 🔜 Add
datefield to theUserDataobject as this is needed for proper signing of commits. (Fixed by #485) - ⚙️ Finish the signature signing (either in this package or a companion package; @chingor13 do you have a preference for where to put the commit signing algorithm? It's not trivial to figure out this algorithm, so a helper might be useful here)
- Extend
release-pleaseto allow passing GPG private key and optional passphrase. - Extend the
release-pleaseGitHub action to allow passing GPG private key and optional passphrase.
Okay, a quick update from my side. I've created the PR that allows commit signing (see example below): #485
The commit signing was done using this signer:
class GPGCommitSigner implements CommitSigner {
private privateKey: string;
private passphrase: string | undefined;
constructor({
privateKey,
passphrase,
}: {
privateKey: string;
passphrase?: string;
}) {
this.privateKey = privateKey;
this.passphrase = passphrase;
}
async generateSignature(commit: CommitDataWithRequiredDate): Promise<string> {
const privateKey = await openpgp.readPrivateKey({
armoredKey: this.privateKey,
});
const commitObject = this.buildCommitObject(commit);
const message = await openpgp.createCleartextMessage({
text: commitObject,
});
const signedMessage = await openpgp.sign({
message,
signingKeys: privateKey,
// todo: Figure out how to use passphrases
});
return this.parseSignature(signedMessage);
}
private buildCommitObject(commit: CommitDataWithRequiredDate) {
const rows = [];
rows.push(`tree ${commit.tree}`);
for (const parent of commit.parents) {
rows.push(`parent ${parent}`);
}
if (commit.author) {
rows.push(`author ${this.buildUserCommitObjectRow(commit.author)}`);
}
if (commit.committer) {
rows.push(`committer ${this.buildUserCommitObjectRow(commit.committer)}`);
}
rows.push('');
rows.push(commit.message);
return rows.join('\n');
}
private buildUserCommitObjectRow({
name,
email,
date,
}: {
name: string;
email: string;
date: Date;
}) {
const unixDate = Math.floor(date.getTime() / 1000);
const timezoneOffset = this.buildTimezoneOffset(date);
return `${name} <${email}> ${unixDate} ${timezoneOffset}`;
}
private buildTimezoneOffset(date: Date): string {
const timezoneOffset = date.getTimezoneOffset();
const timezoneOffsetAbsolute = Math.abs(timezoneOffset);
if (timezoneOffset === 0) {
return '+0000';
}
const offsetInHourFormat = (timezoneOffsetAbsolute / 60) * 100;
const offsetString = String(offsetInHourFormat).padStart(4, '0');
if (timezoneOffset < 0) {
return `+${offsetString}`;
}
return `-${offsetString}`;
}
private parseSignature(signedMessage: string): string {
const signature =
'-----BEGIN PGP SIGNATURE-----' +
signedMessage.split('-----BEGIN PGP SIGNATURE-----')[1];
return signature;
}
}
And the generated commit is verified as you can see here: https://github.com/simpleclub-extended/code-suggester/pull/2/commits/ca1c82b7559f28d730b18a56a75c86a910d677a3
I called the function like this:
const octokit = new Octokit();
const fileData = new FileData('hello world', '100644');
const changes = new Map();
changes.set('foo.md', fileData);
await suggester.createPullRequest(octokit, changes, {
upstreamOwner: 'simpleclub-extended',
upstreamRepo: 'code-suggester',
title: 'Test commit signing using GPG keys',
message: 'Test signed commit',
description: '',
fork: false,
force: true,
author: {
name: '<name>',
email: '<email>',
},
committer: {
name: '<name>',
email: '<email>',
},
signer: new GPGCommitSigner({privateKey: privateKeyArmored}),
});
@chingor13 could you review my PR to expose the date and let me know if you have a preference for where to best put the GPGCommitSigner class?