Blob Blame History Raw
# HG changeset patch
# User Kai Engert <kaie@kuix.de>
# 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