aboutsummaryrefslogtreecommitdiff
path: root/packages/server/src/git
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2021-08-11 12:59:23 +0200
committerHampusM <hampus@hampusmat.com>2021-08-11 12:59:23 +0200
commit108b469cbb09f911c9a52d4134a504d9b51ac30d (patch)
tree309763642bf53b4adecf20bfd09047715f572c39 /packages/server/src/git
parent3eb8757b8db81476782870d460e3d856907186a7 (diff)
Implemented gpg signed commit support
Diffstat (limited to 'packages/server/src/git')
-rw-r--r--packages/server/src/git/commit.ts114
-rw-r--r--packages/server/src/git/error.ts6
2 files changed, 114 insertions, 6 deletions
diff --git a/packages/server/src/git/commit.ts b/packages/server/src/git/commit.ts
index cf1ac5a..32b5869 100644
--- a/packages/server/src/git/commit.ts
+++ b/packages/server/src/git/commit.ts
@@ -3,6 +3,13 @@ import { Author } from "api";
import { Diff } from "./diff";
import { Repository } from "./repository";
import { Tree } from "./tree";
+import { createMessage, readKey, readSignature, verify } from "openpgp";
+import { promisify } from "util";
+import { exec } from "child_process";
+import { findAsync } from "./misc";
+import { CommitError, createError } from "./error";
+
+const pExec = promisify(exec);
export type CommitSummary = {
id: string,
@@ -17,6 +24,89 @@ type DiffStats = {
}
/**
+ * A author of a commit
+ */
+export class CommitAuthor implements Author {
+ private _ng_commit: NodeGitCommit;
+
+ constructor(ng_commit: NodeGitCommit) {
+ this._ng_commit = ng_commit;
+ }
+
+ public get name(): string {
+ return this._ng_commit.author().name();
+ }
+
+ public get email(): string {
+ return this._ng_commit.author().email();
+ }
+
+ /**
+ * Returns the public key fingerprint of the commit's signer.
+ */
+ public async fingerprint(): Promise<string> {
+ const basic_signature = await this._ng_commit.getSignature().catch(() => {
+ throw(createError(CommitError, 500, "Commit isn't signed!"));
+ });
+
+ const message = await createMessage({ text: basic_signature.signedData });
+
+ const pub_keys_list = await pExec("gpg --list-public-keys");
+
+ if(pub_keys_list.stderr) {
+ throw(createError(CommitError, 500, "Failed to get public keys from gpg!"));
+ }
+
+ const pub_keys = pub_keys_list.stdout
+ .split("\n")
+ .slice(2, -1)
+ .join("\n")
+ .split(/^\n/gm);
+
+ // Find a public key that matches the signature
+ const pub_key = await findAsync(pub_keys, async key => {
+ // Make sure the UID is the same as the commit author
+ const uid = key
+ .split("\n")[2]
+ .replace(/^uid\s*\[.*\]\s/, "");
+
+ if(uid !== `${this.name} <${this.email}>`) {
+ return false;
+ }
+
+ // Get the public key as an armored key
+ const fingerprint = key.split("\n")[1].replace(/^\s*/, "");
+ const key_export = await pExec(`gpg --armor --export ${fingerprint}`);
+
+ if(key_export.stderr) {
+ throw(createError(CommitError, 500, "Failed to export a public key from gpg!"));
+ }
+
+ const signature = await readSignature({ armoredSignature: basic_signature.signature });
+
+ const verification = await verify({
+ message: message,
+ verificationKeys: await readKey({ armoredKey: key_export.stdout }),
+ expectSigned: true,
+ signature: signature
+ })
+ .then(result => result.signatures[0].verified)
+ .catch(() => Promise.resolve(false));
+
+ return verification;
+ });
+
+ if(!pub_key) {
+ throw(createError(CommitError, 500, "Failed to find a public key matching the commit signature!"));
+ }
+
+ return pub_key
+ .split("\n")[1]
+ .replace(/^\s*/, "");
+ }
+}
+
+/**
* A representation of a commit
*/
export class Commit {
@@ -24,7 +114,6 @@ export class Commit {
private _owner: Repository;
public id: string;
- public author: Author;
public date: number;
public message: string;
@@ -35,17 +124,21 @@ export class Commit {
constructor(owner: Repository, commit: NodeGitCommit) {
this._ng_commit = commit;
this._owner = owner;
-
this.id = commit.sha();
- this.author = {
- name: commit.author().name(),
- email: commit.author().email()
- };
this.date = commit.time();
this.message = commit.message();
}
/**
+ * Returns the commit's author
+ *
+ * @returns An instance of a commit author
+ */
+ public author(): CommitAuthor {
+ return new CommitAuthor(this._ng_commit);
+ }
+
+ /**
* Returns the commit's diff
*
* @returns An instance of a diff
@@ -79,6 +172,15 @@ export class Commit {
}
/**
+ * Returns whether or not the commit is signed
+ */
+ public isSigned(): Promise<boolean> {
+ return this._ng_commit.getSignature()
+ .then(() => true)
+ .catch(() => false);
+ }
+
+ /**
* Lookup a commit
*
* @param repository - The repository which the commit is in
diff --git a/packages/server/src/git/error.ts b/packages/server/src/git/error.ts
index 6715d8f..b89b994 100644
--- a/packages/server/src/git/error.ts
+++ b/packages/server/src/git/error.ts
@@ -54,6 +54,12 @@ export class DiffError extends BaseError {
}
}
+export class CommitError extends BaseError {
+ constructor(code: number, message: string) {
+ super(code, "A commit error has occured: " + message);
+ }
+}
+
type ErrorConstructorType<T> = new (code: number, message: string) => T;
/**