Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GPG Signature option #337

Open
chingor13 opened this issue Feb 24, 2022 · 4 comments · May be fixed by #485
Open

Add GPG Signature option #337

chingor13 opened this issue Feb 24, 2022 · 4 comments · May be fixed by #485
Labels
help wanted We'd love to have community involvement on this issue. priority: p3 Desirable enhancement or fix. May not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Comments

@chingor13
Copy link
Contributor

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

@chingor13 chingor13 added type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. priority: p3 Desirable enhancement or fix. May not be included in next release. labels Feb 24, 2022
@chingor13 chingor13 added the help wanted We'd love to have community involvement on this issue. label Aug 19, 2022
@IchordeDionysos
Copy link

Is there a reason this PR is still open, isn't this now supported? @chingor13

@Zebradil
Copy link

Hi @IchordeDionysos,

Could you tell how to configure release-please to sign commits?

@IchordeDionysos
Copy link

IchordeDionysos commented Jan 30, 2024

@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 openpgp library to sign commits.
  • I can construct most of the commit payload required for signing
  • The only missing piece in the code-suggester library 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:

  1. 🔜 Add date field to the UserData object as this is needed for proper signing of commits. (Fixed by feat: Create/Pass commit date to commit for signing #485)
  2. ⚙️ 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)
  3. Extend release-please to allow passing GPG private key and optional passphrase.
  4. Extend the release-please GitHub action to allow passing GPG private key and optional passphrase.

@IchordeDionysos
Copy link

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: simpleclub-extended@ca1c82b

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted We'd love to have community involvement on this issue. priority: p3 Desirable enhancement or fix. May not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.
Projects
None yet
3 participants