diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 5fcba297..3404dc6d 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -188,6 +188,7 @@
"Generate PGP Key Pair",
"PGP Encrypt",
"PGP Decrypt",
+ "PGP Sign",
"PGP Verify",
"PGP Encrypt and Sign",
"PGP Decrypt and Verify",
diff --git a/src/core/operations/GeneratePGPKeyPair.mjs b/src/core/operations/GeneratePGPKeyPair.mjs
index a26b5cc8..e3fd6742 100644
--- a/src/core/operations/GeneratePGPKeyPair.mjs
+++ b/src/core/operations/GeneratePGPKeyPair.mjs
@@ -12,6 +12,7 @@ import { getSubkeySize, ASP } from "../lib/PGP.mjs";
import { cryptNotice } from "../lib/Crypt.mjs";
import * as es6promisify from "es6-promisify";
const promisify = es6promisify.default ? es6promisify.default.promisify : es6promisify.promisify;
+const KEY_FLAGS = kbpgp.const.openpgp.key_flags;
/**
@@ -73,11 +74,11 @@ class GeneratePGPKeyPair extends Operation {
if (name) userIdentifier += name;
if (email) userIdentifier += ` <${email}>`;
- let flags = kbpgp.const.openpgp.certify_keys;
- flags |= kbpgp.const.openpgp.sign_data;
- flags |= kbpgp.const.openpgp.auth;
- flags |= kbpgp.const.openpgp.encrypt_comm;
- flags |= kbpgp.const.openpgp.encrypt_storage;
+ let flags = KEY_FLAGS.certify_keys;
+ flags |= KEY_FLAGS.sign_data;
+ flags |= KEY_FLAGS.auth;
+ flags |= KEY_FLAGS.encrypt_comm;
+ flags |= KEY_FLAGS.encrypt_storage;
const keyGenerationOptions = {
userid: userIdentifier,
@@ -89,11 +90,11 @@ class GeneratePGPKeyPair extends Operation {
},
subkeys: [{
"nbits": getSubkeySize(keySize),
- "flags": kbpgp.const.openpgp.sign_data,
+ "flags": KEY_FLAGS.sign_data,
"expire_in": 86400 * 365 * 8
}, {
"nbits": getSubkeySize(keySize),
- "flags": kbpgp.const.openpgp.encrypt_comm | kbpgp.const.openpgp.encrypt_storage,
+ "flags": KEY_FLAGS.encrypt_comm | KEY_FLAGS.encrypt_storage,
"expire_in": 86400 * 365 * 2
}],
asp: ASP
diff --git a/src/core/operations/PGPSign.mjs b/src/core/operations/PGPSign.mjs
new file mode 100644
index 00000000..e3063d89
--- /dev/null
+++ b/src/core/operations/PGPSign.mjs
@@ -0,0 +1,83 @@
+/**
+ * @author GCHQDeveloper581
+ * @copyright Crown Copyright 2026
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import kbpgp from "kbpgp";
+import { ASP, importPrivateKey } from "../lib/PGP.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import * as es6promisify from "es6-promisify";
+const promisify = es6promisify.default ? es6promisify.default.promisify : es6promisify.promisify;
+
+/**
+ * PGP Sign operation
+ */
+class PGPSign extends Operation {
+
+ /**
+ * PGPSign constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "PGP Sign";
+ this.module = "PGP";
+ this.description = [
+ "Input: the message you want to sign",
+ "
",
+ "Arguments: the ASCII-armoured PGP private key of the sender.",
+ "
",
+ "Pretty Good Privacy is an encryption standard (OpenPGP) used for encrypting, decrypting, and signing messages.",
+ "
",
+ "This function uses the Keybase implementation of PGP.",
+ ].join("\n");
+ this.infoURL = "https://wikipedia.org/wiki/Pretty_Good_Privacy"; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc)
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Private key of signer",
+ "type": "text",
+ "value": ""
+ },
+ {
+ "name": "Private key passphrase (optional)",
+ "type": "string",
+ "value": ""
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ *
+ * @throws {OperationError} if failed private key import or failed encryption
+ */
+ async run(input, args) {
+ const message = input,
+ [privateKey, passphrase] = args;
+ let signedMessage;
+
+ if (!privateKey) throw new OperationError("Enter the private key of the signer.");
+ const privKey = await importPrivateKey(privateKey, passphrase);
+
+ try {
+ signedMessage = await promisify(kbpgp.box)({
+ "msg": message,
+ "sign_with": privKey,
+ "asp": ASP
+ });
+ } catch (err) {
+ throw new OperationError(`Couldn't sign message: ${err}`);
+ }
+
+ return signedMessage;
+ }
+
+}
+
+export default PGPSign;
diff --git a/tests/node/index.mjs b/tests/node/index.mjs
index f872f8f4..52670d48 100644
--- a/tests/node/index.mjs
+++ b/tests/node/index.mjs
@@ -18,6 +18,7 @@ import {
import TestRegister from "../lib/TestRegister.mjs";
import "./tests/nodeApi.mjs";
import "./tests/operations.mjs";
+import "./tests/PGP.mjs";
import "./tests/File.mjs";
import "./tests/Dish.mjs";
import "./tests/NodeDish.mjs";
diff --git a/tests/node/tests/PGP.mjs b/tests/node/tests/PGP.mjs
new file mode 100644
index 00000000..2a695ea3
--- /dev/null
+++ b/tests/node/tests/PGP.mjs
@@ -0,0 +1,69 @@
+/**
+ * PGP node tests.
+ *
+ * @author C85297 [95289555+C85297@users.noreply.github.com]
+ * @copyright Crown Copyright 2026
+ * @license Apache-2.0
+ */
+
+import assert from "assert";
+import kbpgp from "kbpgp";
+import * as es6promisify from "es6-promisify";
+
+import TestRegister from "../../lib/TestRegister.mjs";
+import it from "../assertionHandler.mjs";
+import GeneratePGPKeyPair from "../../../src/core/operations/GeneratePGPKeyPair.mjs";
+
+const promisify = es6promisify.default ? es6promisify.default.promisify : es6promisify.promisify;
+
+const PUBLIC_KEY_BLOCK = /-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*-----END PGP PUBLIC KEY BLOCK-----/;
+
+/**
+ * Generate a PGP key pair and import the generated public key.
+ *
+ * @param {string} keyType
+ * @returns {Promise