# HG changeset patch # User Kai Engert # Date 1666897160 -7200 # Thu Oct 27 20:59:20 2022 +0200 # Node ID af0b1f5e4c7710f824c6141103e516ca60bc78aa # Parent adfbf6378df82c8b2e087427a48ddc5cbe13aadd Bug 1791195 - Add RNP security rules to obsolete our patches to RNP. r=mkmelin,o.nickolay diff --git a/comm/mail/extensions/openpgp/content/modules/RNP.jsm b/comm/mail/extensions/openpgp/content/modules/RNP.jsm --- a/comm/mail/extensions/openpgp/content/modules/RNP.jsm +++ b/comm/mail/extensions/openpgp/content/modules/RNP.jsm @@ -1863,12 +1863,12 @@ var RNP = { if (keyBlockStr.length > RNP.maxImportKeyBlockSize) { throw new Error("rejecting big keyblock"); } - let tempFFI = new RNPLib.rnp_ffi_t(); - if (RNPLib.rnp_ffi_create(tempFFI.address(), "GPG", "GPG")) { + let tempFFI = RNPLib.prepare_ffi(); + if (!tempFFI) { throw new Error("Couldn't initialize librnp."); } let pubKey; if (!this.importToFFI(tempFFI, keyBlockStr, true, false, permissive)) { @@ -1892,12 +1892,12 @@ var RNP = { if (keyBlockStr.length > RNP.maxImportKeyBlockSize) { throw new Error("rejecting big keyblock"); } - let tempFFI = new RNPLib.rnp_ffi_t(); - if (RNPLib.rnp_ffi_create(tempFFI.address(), "GPG", "GPG")) { + let tempFFI = RNPLib.prepare_ffi(); + if (!tempFFI) { throw new Error("Couldn't initialize librnp."); } let keyList = null; if (!this.importToFFI(tempFFI, keyBlockStr, pubkey, seckey, permissive)) { @@ -1929,12 +1929,12 @@ var RNP = { async mergePublicKeyBlocks(fingerprint, ...keyBlocks) { if (keyBlocks.some(b => b.length > RNP.maxImportKeyBlockSize)) { throw new Error("keyBlock too big"); } - let tempFFI = new RNPLib.rnp_ffi_t(); - if (RNPLib.rnp_ffi_create(tempFFI.address(), "GPG", "GPG")) { + let tempFFI = RNPLib.prepare_ffi(); + if (!tempFFI) { throw new Error("Couldn't initialize librnp."); } const pubkey = true; const seckey = false; @@ -2067,12 +2067,12 @@ var RNP = { let result = {}; result.exitCode = -1; result.importedKeys = []; result.errorMsg = ""; - let tempFFI = new RNPLib.rnp_ffi_t(); - if (RNPLib.rnp_ffi_create(tempFFI.address(), "GPG", "GPG")) { + let tempFFI = RNPLib.prepare_ffi(); + if (!tempFFI) { throw new Error("Couldn't initialize librnp."); } // TODO: check result if (this.importToFFI(tempFFI, keyBlockStr, pubkey, seckey, permissive)) { @@ -3115,12 +3115,12 @@ var RNP = { * */ export_pubkey_strip_sigs_uids(expKey, keepUserIDs, out_binary) { let expKeyId = this.getKeyIDFromHandle(expKey); - let tempFFI = new RNPLib.rnp_ffi_t(); - if (RNPLib.rnp_ffi_create(tempFFI.address(), "GPG", "GPG")) { + let tempFFI = RNPLib.prepare_ffi(); + if (!tempFFI) { throw new Error("Couldn't initialize librnp."); } let exportFlags = RNPLib.RNP_KEY_EXPORT_SUBKEYS | RNPLib.RNP_KEY_EXPORT_PUBLIC; @@ -3399,12 +3399,12 @@ var RNP = { )) ) { throw new Error("rnp_output_to_armor failed:" + rv); } - let tempFFI = new RNPLib.rnp_ffi_t(); - if (RNPLib.rnp_ffi_create(tempFFI.address(), "GPG", "GPG")) { + let tempFFI = RNPLib.prepare_ffi(); + if (!tempFFI) { throw new Error("Couldn't initialize librnp."); } let internalPassword = await OpenPGPMasterpass.retrieveOpenPGPPassword(); diff --git a/comm/mail/extensions/openpgp/content/modules/RNPLib.jsm b/mail/extensions/openpgp/content/modules/RNPLib/comm.jsm --- a/comm/mail/extensions/openpgp/content/modules/RNPLib.jsm +++ b/comm/mail/extensions/openpgp/content/modules/RNPLib.jsm @@ -13,11 +13,11 @@ XPCOMUtils.defineLazyModuleGetters(this, OpenPGPMasterpass: "chrome://openpgp/content/modules/masterpass.jsm", Services: "resource://gre/modules/Services.jsm", setTimeout: "resource://gre/modules/Timer.jsm", }); -const MIN_RNP_VERSION = [0, 16, 0]; +const MIN_RNP_VERSION = [0, 16, 2]; var systemOS = Services.appinfo.OS.toLowerCase(); var abi = ctypes.default_abi; // Open librnp. Determine the path to the chrome directory and look for it @@ -149,10 +149,12 @@ function enableRNPLibJS() { // this must be delayed until after "librnp" is initialized RNPLib = { path: librnpPath, + // Handle to the RNP library and primary key data store. + // Kept at null if init fails. ffi: null, // returns rnp_input_t, destroy using rnp_input_destroy async createInputFromPath(path) { // IOUtils.read always returns an array. @@ -265,13 +267,204 @@ function enableRNPLibJS() { const min_version = this.rnp_version_for(...MIN_RNP_VERSION); const this_version = this.rnp_version(); return Boolean(this_version >= min_version); }, + /** + * Prepare an RNP library handle, and in addition set all the + * application's preferences for library behavior. + * + * Other application code should NOT call rnp_ffi_create directly, + * but obtain an RNP library handle from this function. + */ + prepare_ffi() { + let ffi = new rnp_ffi_t(); + if (this._rnp_ffi_create(ffi.address(), "GPG", "GPG")) { + return null; + } + + // Treat MD5 as insecure. + if ( + this.rnp_add_security_rule( + ffi, + this.RNP_FEATURE_HASH_ALG, + this.RNP_ALGNAME_MD5, + this.RNP_SECURITY_OVERRIDE, + 0, + this.RNP_SECURITY_INSECURE + ) + ) { + return null; + } + + // Use RNP's default rule for SHA1 used with data signatures, + // and use our override to allow it for key signatures. + if ( + this.rnp_add_security_rule( + ffi, + this.RNP_FEATURE_HASH_ALG, + this.RNP_ALGNAME_SHA1, + this.RNP_SECURITY_VERIFY_KEY | this.RNP_SECURITY_OVERRIDE, + 0, + this.RNP_SECURITY_DEFAULT + ) + ) { + return null; + } + + /* + // Security rules API does not yet support PK and SYMM algs. + // + // If a hash algorithm is already disabled at build time, + // and an attempt is made to set a security rule for that + // algorithm, then RNP returns a failure. + // + // Ideally, RNP should allow these calls (regardless of build time + // settings) to define an application security rule, that is + // independent of the configuration used for building the + // RNP library. + + if ( + this.rnp_add_security_rule( + ffi, + this.RNP_FEATURE_HASH_ALG, + this.RNP_ALGNAME_SM3, + this.RNP_SECURITY_OVERRIDE, + 0, + this.RNP_SECURITY_PROHIBITED + ) + ) { + return null; + } + + if ( + this.rnp_add_security_rule( + ffi, + this.RNP_FEATURE_PK_ALG, + this.RNP_ALGNAME_SM2, + this.RNP_SECURITY_OVERRIDE, + 0, + this.RNP_SECURITY_PROHIBITED + ) + ) { + return null; + } + + if ( + this.rnp_add_security_rule( + ffi, + this.RNP_FEATURE_SYMM_ALG, + this.RNP_ALGNAME_SM4, + this.RNP_SECURITY_OVERRIDE, + 0, + this.RNP_SECURITY_PROHIBITED + ) + ) { + return null; + } + */ + + return ffi; + }, + + /** + * Test the correctness of security rules, in particular, test + * if the given hash algorithm is allowed at the given time. + * + * This is an application consistency test. If the behavior isn't + * according to the expectation, the function throws an error. + * + * @param {string} hashAlg - Test this hash algorithm + * @param {time_t} time - Test status at this timestamp + * @param {boolean} keySigAllowed - Test if using the hash algorithm + * is allowed for signatures found inside OpenPGP keys. + * @param {boolean} dataSigAllowed - Test if using the hash algorithm + * is allowed for signatures on data. + */ + _confirmSecurityRule(hashAlg, time, keySigAllowed, dataSigAllowed) { + let level = new ctypes.uint32_t(); + let flag = new ctypes.uint32_t(); + + flag.value = this.RNP_SECURITY_VERIFY_DATA; + let testDataSuccess = false; + if ( + !RNPLib.rnp_get_security_rule( + this.ffi, + this.RNP_FEATURE_HASH_ALG, + hashAlg, + time, + flag.address(), + null, + level.address() + ) + ) { + if (dataSigAllowed) { + testDataSuccess = level.value == RNPLib.RNP_SECURITY_DEFAULT; + } else { + testDataSuccess = level.value < RNPLib.RNP_SECURITY_DEFAULT; + } + } + + if (!testDataSuccess) { + throw new Error("security configuration for data signatures failed"); + } + + flag.value = this.RNP_SECURITY_VERIFY_KEY; + let testKeySuccess = false; + if ( + !RNPLib.rnp_get_security_rule( + this.ffi, + this.RNP_FEATURE_HASH_ALG, + hashAlg, + time, + flag.address(), + null, + level.address() + ) + ) { + if (keySigAllowed) { + testKeySuccess = level.value == RNPLib.RNP_SECURITY_DEFAULT; + } else { + testKeySuccess = level.value < RNPLib.RNP_SECURITY_DEFAULT; + } + } + + if (!testKeySuccess) { + throw new Error("security configuration for key signatures failed"); + } + }, + + /** + * Perform tests that the RNP library behaves according to the + * defined security rules. + * If a problem is found, the function throws an error. + */ + _sanityCheckSecurityRules() { + let time_t_now = Math.round(Date.now() / 1000); + let ten_years_in_seconds = 10 * 365 * 24 * 60 * 60; + let ten_years_future = time_t_now + ten_years_in_seconds; + + this._confirmSecurityRule(this.RNP_ALGNAME_MD5, time_t_now, false, false); + this._confirmSecurityRule( + this.RNP_ALGNAME_MD5, + ten_years_future, + false, + false + ); + + this._confirmSecurityRule(this.RNP_ALGNAME_SHA1, time_t_now, true, false); + this._confirmSecurityRule( + this.RNP_ALGNAME_SHA1, + ten_years_future, + true, + false + ); + }, + async init() { - this.ffi = new rnp_ffi_t(); - if (this.rnp_ffi_create(this.ffi.address(), "GPG", "GPG")) { + this.ffi = this.prepare_ffi(); + if (!this.ffi) { throw new Error("Couldn't initialize librnp."); } this.rnp_ffi_set_log_fd(this.ffi, 2); // stderr @@ -286,10 +479,18 @@ function enableRNPLibJS() { null ); let { pubRingPath, secRingPath } = this.getFilenames(); + try { + this._sanityCheckSecurityRules(); + } catch (e) { + // Disable all RNP operation + this.ffi = null; + throw e; + } + await this.loadWithFallback(pubRingPath, this.RNP_LOAD_SAVE_PUBLIC_KEYS); await this.loadWithFallback(secRingPath, this.RNP_LOAD_SAVE_SECRET_KEYS); let pubnum = new ctypes.size_t(); this.rnp_get_public_key_count(this.ffi, pubnum.address()); @@ -481,10 +682,14 @@ function enableRNPLibJS() { * @param {string} path - The file path to save to. * @param {number} keyRingFlag - RNP_LOAD_SAVE_PUBLIC_KEYS or * RNP_LOAD_SAVE_SECRET_KEYS. */ async saveKeyRing(path, keyRingFlag) { + if (!this.ffi) { + return; + } + let oldPath = path + ".old"; // Ignore failure, oldPath might not exist yet. await IOUtils.copy(path, oldPath).catch(() => {}); @@ -540,10 +745,13 @@ function enableRNPLibJS() { tmpPath: path + ".tmp-new", }); }, async saveKeys() { + if (!this.ffi) { + return; + } let { pubRingPath, secRingPath } = this.getFilenames(); let saveThem = async () => { await this.saveKeyRing(pubRingPath, this.RNP_LOAD_SAVE_PUBLIC_KEYS); await this.saveKeyRing(secRingPath, this.RNP_LOAD_SAVE_SECRET_KEYS); @@ -600,11 +808,13 @@ function enableRNPLibJS() { abi, ctypes.char.ptr ), // Get a RNP library handle. - rnp_ffi_create: librnp.declare( + // Mark with leading underscore, to clarify that this function + // shouldn't be called directly - you should call prepare_ffi(). + _rnp_ffi_create: librnp.declare( "rnp_ffi_create", abi, rnp_result_t, rnp_ffi_t.ptr, ctypes.char.ptr, @@ -1713,10 +1923,22 @@ function enableRNPLibJS() { ctypes.uint32_t.ptr, ctypes.uint64_t.ptr, ctypes.uint32_t.ptr ), + rnp_add_security_rule: librnp.declare( + "rnp_add_security_rule", + abi, + rnp_result_t, + rnp_ffi_t, + ctypes.char.ptr, + ctypes.char.ptr, + ctypes.uint32_t, + ctypes.uint64_t, + ctypes.uint32_t + ), + rnp_result_t, rnp_ffi_t, rnp_password_cb_t, rnp_input_t, rnp_output_t, @@ -1748,11 +1970,26 @@ function enableRNPLibJS() { RNP_KEY_SIGNATURE_NON_SELF_SIG: 4, RNP_SUCCESS: 0x00000000, + RNP_FEATURE_SYMM_ALG: "symmetric algorithm", RNP_FEATURE_HASH_ALG: "hash algorithm", + RNP_FEATURE_PK_ALG: "public key algorithm", + RNP_ALGNAME_MD5: "MD5", + RNP_ALGNAME_SHA1: "SHA1", + RNP_ALGNAME_SM2: "SM2", + RNP_ALGNAME_SM3: "SM3", + RNP_ALGNAME_SM4: "SM4", + + RNP_SECURITY_OVERRIDE: 1, + RNP_SECURITY_VERIFY_KEY: 2, + RNP_SECURITY_VERIFY_DATA: 4, + RNP_SECURITY_REMOVE_ALL: 65536, + + RNP_SECURITY_PROHIBITED: 0, + RNP_SECURITY_INSECURE: 1, RNP_SECURITY_DEFAULT: 2, /* Common error codes */ RNP_ERROR_GENERIC: 0x10000000, // 268435456 RNP_ERROR_BAD_FORMAT: 0x10000001, // 268435457