Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Tests signing with a secret key that is managed by an external GnuPG
* installation, which we drive through GPGME.
*
* A gpg executable and a loadable gpgme library are required. If either
* is missing, the tasks are skipped. If both are present, the results
* are checked strictly.
*
* The external GnuPG is configured for RFC 4880 compliance, tests must
* not depend on LibrePGP extensions.
*/
"use strict";
const { RNP } = ChromeUtils.importESModule(
);
const { GPGME } = ChromeUtils.importESModule(
);
const { EnigmailConstants } = ChromeUtils.importESModule(
);
const { OpenPGPTestUtils } = ChromeUtils.importESModule(
);
const { Subprocess } = ChromeUtils.importESModule(
);
const keyDir = "../../../../../test/browser/openpgp/data/keys";
// Bob's primary key is capable of signing, it has no signing subkey.
const BOB_PRIMARY_ID = "FBFCC82A015E7330";
const BOB_EMAIL = "bob@openpgp.example";
const BOB_SECRET_FILE = "bob@openpgp.example-0xfbfcc82a015e7330-secret.asc";
// This key's primary is certify-only, signatures must be made by its
// signing subkey.
const SUBKEY_ONLY_PRIMARY_ID = "5F97DC942D7D765E";
const SUBKEY_ONLY_SIGNING_ID = "616883D3C579E402";
const SUBKEY_ONLY_EMAIL = "sign-subkey-only@openpgp.example";
// Keep this on a single line, GPGME signs detached signatures in text
// mode, which canonicalizes line endings.
const PLAINTEXT = "External GnuPG signing test.";
let gpgPath = null;
let gnupgHome = null;
let externalGnuPGReady = false;
/**
* Run the external gpg executable against our temporary GnuPG home.
*
* @param {string[]} args - Command line arguments.
* @returns {Promise<{exitCode: integer, stderr: string}>}
*/
async function runGpg(args) {
const process = await Subprocess.call({
command: gpgPath,
arguments: ["--batch", "--no-tty", "--yes", "--rfc4880", ...args],
environment: { GNUPGHOME: gnupgHome.path },
environmentAppend: true,
stderr: "pipe",
});
const stderr = await process.stderr.readString();
const { exitCode } = await process.wait();
return { exitCode, stderr };
}
/**
* Stop the gpg-agent that was started for our temporary GnuPG home,
* otherwise it keeps running after the test has ended.
*/
async function killGnuPGAgent() {
let gpgconfPath;
try {
gpgconfPath = await Subprocess.pathSearch("gpgconf");
} catch (ex) {
return;
}
const process = await Subprocess.call({
command: gpgconfPath,
arguments: ["--kill", "all"],
environment: { GNUPGHOME: gnupgHome.path },
environmentAppend: true,
stderr: "pipe",
});
await process.stderr.readString();
await process.wait();
}
/**
* @param {string} sender - Key ID of the sender, prefixed with 0x.
* @returns {object} arguments for RNP.encryptAndOrSign
*/
function detachedSignArgs(sender) {
return {
aliasKeys: new Map(),
armor: true,
bcc: [],
encrypt: false,
encryptToSender: false,
sender,
senderKeyIsExternal: true,
sigTypeClear: false,
sigTypeDetached: true,
sign: true,
signatureHash: "SHA256",
to: [],
};
}
add_setup(async function () {
do_get_profile();
await OpenPGPTestUtils.initOpenPGP();
Services.prefs.setBoolPref("mail.openpgp.allow_external_gnupg", true);
// Our own keyring gets Bob's secret key, which is used to decrypt the
// combined encrypt+sign result, and the public key of the externally
// managed key, which is all we would have in the external scenario.
await OpenPGPTestUtils.importPrivateKey(
null,
do_get_file(`${keyDir}/${BOB_SECRET_FILE}`)
);
await OpenPGPTestUtils.importPublicKey(
null,
do_get_file(`${keyDir}/sign-subkey-only-pub.asc`)
);
try {
gpgPath = await Subprocess.pathSearch("gpg");
} catch (ex) {
info("No gpg executable found");
return;
}
info(`Using gpg executable ${gpgPath}`);
// Not the profile directory, the path of the gpg-agent socket below
// it must stay short enough for a unix domain socket.
gnupgHome = do_get_tempdir();
gnupgHome.append("gnupg");
gnupgHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
const previousHome = Services.env.exists("GNUPGHOME")
? Services.env.get("GNUPGHOME")
: "";
Services.env.set("GNUPGHOME", gnupgHome.path);
registerCleanupFunction(async function () {
await killGnuPGAgent();
Services.env.set("GNUPGHOME", previousHome);
gnupgHome.remove(true);
});
// The digest must be pinned, otherwise the digest that gpg picks
// depends on its version and on the key's preferences, and RNP
// rejects a SHA-1 signature.
await IOUtils.writeUTF8(
PathUtils.join(gnupgHome.path, "gpg.conf"),
"compliance rfc4880\ndigest-algo SHA256\n"
);
if (!GPGME.init()) {
info("No usable gpgme library found");
return;
}
for (const keyFile of [BOB_SECRET_FILE, "sign-subkey-only-secret.asc"]) {
const { exitCode, stderr } = await runGpg([
"--import",
do_get_file(`${keyDir}/${keyFile}`).path,
]);
Assert.equal(exitCode, 0, `gpg should import ${keyFile}, got: ${stderr}`);
}
externalGnuPGReady = true;
});
/**
* Base roundtrip: have the external GnuPG produce a detached signature
* with a signing capable primary key, and verify it with RNP.
*/
add_task(
{ skip_if: () => !externalGnuPGReady },
async function testExternalDetachedSignature() {
const args = detachedSignArgs(`0x${BOB_PRIMARY_ID}`);
const resultStatus = {};
const signature = await RNP.encryptAndOrSign(PLAINTEXT, args, resultStatus);
Assert.equal(resultStatus.exitCode, 0, "external signing should succeed");
Assert.ok(
resultStatus.statusFlags & EnigmailConstants.SIG_CREATED,
"a signature should have been created"
);
Assert.ok(
signature.startsWith("-----BEGIN PGP SIGNATURE-----"),
"result should be an armored detached signature"
);
Assert.equal(
args.externalSenderSigningKeyID,
BOB_PRIMARY_ID,
"the primary key should be used, it's the only signing capable key"
);
const verified = await RNP.verifyDetached(
PLAINTEXT,
signature,
BOB_EMAIL,
null
);
Assert.equal(verified.exitCode, 0, "verification should succeed");
Assert.ok(
verified.statusFlags & EnigmailConstants.GOOD_SIGNATURE,
"signature should be good"
);
Assert.equal(
verified.keyId,
BOB_PRIMARY_ID,
"signature should have been made by the primary key"
);
}
);
/**
* If the primary key of the externally managed key cannot sign, the
* signing subkey must be used. GPGME cannot pick the subkey on its own,
* we must give it the key ID of the subkey. (Bug 2026356)
*/
add_task(
{ skip_if: () => !externalGnuPGReady },
async function testExternalSigningSubkey() {
const args = detachedSignArgs(`0x${SUBKEY_ONLY_PRIMARY_ID}`);
const resultStatus = {};
const signature = await RNP.encryptAndOrSign(PLAINTEXT, args, resultStatus);
Assert.equal(resultStatus.exitCode, 0, "external signing should succeed");
Assert.equal(
args.externalSenderSigningKeyID,
SUBKEY_ONLY_SIGNING_ID,
"the signing subkey should be used, the primary cannot sign"
);
const verified = await RNP.verifyDetached(
PLAINTEXT,
signature,
SUBKEY_ONLY_EMAIL,
null
);
Assert.equal(verified.exitCode, 0, "verification should succeed");
Assert.ok(
verified.statusFlags & EnigmailConstants.GOOD_SIGNATURE,
"signature should be good"
);
Assert.equal(
verified.keyId,
SUBKEY_ONLY_SIGNING_ID,
"signature should have been made by the signing subkey"
);
}
);
/**
* Combined signing and encryption, the external GnuPG produces the
* inner signed message, RNP encrypts it.
*/
add_task(
{ skip_if: () => !externalGnuPGReady },
async function testExternalSignAndEncrypt() {
const args = {
aliasKeys: new Map(),
armor: true,
bcc: [],
encrypt: true,
encryptToSender: false,
sender: `0x${SUBKEY_ONLY_PRIMARY_ID}`,
senderKeyIsExternal: true,
sigTypeClear: false,
sigTypeDetached: false,
sign: true,
signatureHash: "SHA256",
to: [`<${BOB_EMAIL}>`],
};
const resultStatus = {};
const encrypted = await RNP.encryptAndOrSign(PLAINTEXT, args, resultStatus);
Assert.equal(resultStatus.exitCode, 0, "encryption should succeed");
Assert.equal(
args.externalSenderSigningKeyID,
SUBKEY_ONLY_SIGNING_ID,
"the signing subkey should be used, the primary cannot sign"
);
const result = await RNP.decrypt(encrypted, {
fromAddr: SUBKEY_ONLY_EMAIL,
maxOutputLength: encrypted.length * 100,
msgDate: null,
noOutput: false,
uiFlags: EnigmailConstants.UI_PGP_MIME,
verifyOnly: false,
});
Assert.equal(result.exitCode, 0, "decryption should succeed");
Assert.equal(result.decryptedData, PLAINTEXT, "text should roundtrip");
Assert.ok(
result.statusFlags & EnigmailConstants.GOOD_SIGNATURE,
"signature should be good"
);
Assert.equal(
result.keyId,
SUBKEY_ONLY_SIGNING_ID,
"signature should have been made by the signing subkey"
);
}
);