aboutsummaryrefslogtreecommitdiff
path: root/packages
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
parent3eb8757b8db81476782870d460e3d856907186a7 (diff)
Implemented gpg signed commit support
Diffstat (limited to 'packages')
-rw-r--r--packages/api/src/commit.d.ts12
-rw-r--r--packages/api/src/misc.d.ts4
-rw-r--r--packages/server/package.json3
-rw-r--r--packages/server/src/git/commit.ts114
-rw-r--r--packages/server/src/git/error.ts6
-rw-r--r--packages/server/src/routes/api/v1/repo/log.ts6
-rw-r--r--packages/server/src/routes/api/v1/repo/map.ts6
7 files changed, 133 insertions, 18 deletions
diff --git a/packages/api/src/commit.d.ts b/packages/api/src/commit.d.ts
index 6eb598c..0a76f8a 100644
--- a/packages/api/src/commit.d.ts
+++ b/packages/api/src/commit.d.ts
@@ -18,9 +18,13 @@ export type Patch = {
too_large: boolean,
hunks: Hunk[]
}
+export interface CommitAuthor extends Author {
+ fingerprint: string | null
+}
export interface Commit {
message: string,
- author: Author,
+ author: CommitAuthor,
+ isSigned: boolean,
date: number,
insertions: number,
deletions: number,
@@ -30,10 +34,8 @@ export interface Commit {
export type LogCommit = {
id: string,
- author: {
- name: string,
- email: string
- },
+ author: CommitAuthor,
+ isSigned: boolean,
message: string,
date: number,
insertions: number,
diff --git a/packages/api/src/misc.d.ts b/packages/api/src/misc.d.ts
index e43fae5..70ab631 100644
--- a/packages/api/src/misc.d.ts
+++ b/packages/api/src/misc.d.ts
@@ -1,4 +1,4 @@
-export type Author = {
+export interface Author {
name: string,
- email: string
+ email: string,
} \ No newline at end of file
diff --git a/packages/server/package.json b/packages/server/package.json
index a29aecc..8ffb19b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -14,6 +14,7 @@
"fastify-static": "^4.2.2",
"js-yaml": "^4.1.0",
"nodegit": "^0.27.0",
+ "openpgp": "^5.0.0-5",
"tar-stream": "^2.2.0",
"whatwg-url": "^9.0.0"
},
@@ -25,6 +26,7 @@
"@types/whatwg-url": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
+ "api": "^1.0.0",
"eslint": "^7.31.0",
"eslint-config-base": " ^1.0.0",
"eslint-config-standard": "^16.0.3",
@@ -33,7 +35,6 @@
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-tsdoc": "^0.2.14",
"nodemon": "^2.0.12",
- "api": "^1.0.0",
"ts-node": "^10.1.0",
"typescript": "^4.3.2"
}
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;
/**
diff --git a/packages/server/src/routes/api/v1/repo/log.ts b/packages/server/src/routes/api/v1/repo/log.ts
index a7d8dfa..cf53d6b 100644
--- a/packages/server/src/routes/api/v1/repo/log.ts
+++ b/packages/server/src/routes/api/v1/repo/log.ts
@@ -46,9 +46,11 @@ export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, do
const data: APICommit = {
message: commit.message,
author: {
- name: commit.author.name,
- email: commit.author.email
+ name: commit.author().name,
+ email: commit.author().email,
+ fingerprint: await commit.author().fingerprint().catch(() => null)
},
+ isSigned: await commit.isSigned(),
date: commit.date,
insertions: stats.insertions,
deletions: stats.deletions,
diff --git a/packages/server/src/routes/api/v1/repo/map.ts b/packages/server/src/routes/api/v1/repo/map.ts
index a3d0aa8..0544e4f 100644
--- a/packages/server/src/routes/api/v1/repo/map.ts
+++ b/packages/server/src/routes/api/v1/repo/map.ts
@@ -6,9 +6,11 @@ export async function commitMap(commit: Commit): Promise<LogCommit> {
return <LogCommit>{
id: commit.id,
author: {
- name: commit.author.name,
- email: commit.author.email
+ name: commit.author().name,
+ email: commit.author().email,
+ fingerprint: await commit.author().fingerprint().catch(() => null)
},
+ isSigned: await commit.isSigned(),
message: commit.message,
date: commit.date,
insertions: stats.insertions,