Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionInvalid pointer in the Security: PSM component
ComponentSecurity/NSS
Bug ClassLogic Error
Tracker2052134
Fix commit5c2e3f1bea94 (firefox) +47/-8
CISA KEVNot listed
CreditedArtur Cygan of Trail of Bits in partnership with OpenAI
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
add_signature_test
security/manager/ssl/tests/unit/test_signed_apps.js
modified

Files Changed

  • security/manager/ssl/tests/unit/sign_app.py
  • security/manager/ssl/tests/unit/test_signed_apps.js
  • security/manager/ssl/tests/unit/test_signed_apps/moz.build
  • security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip
  • security/manager/tools/pycert.py
diff --git a/security/manager/ssl/tests/unit/sign_app.py b/security/manager/ssl/tests/unit/sign_app.py
index 66dec481008..599c344db1a 100755
--- a/security/manager/ssl/tests/unit/sign_app.py
+++ b/security/manager/ssl/tests/unit/sign_app.py
@@ -10,8 +10,7 @@ resulting zip file. Mainly for creating test inputs to the
 nsIX509CertDB.openSignedAppFileAsync API.
 """
 from base64 import b64encode
-from cbor2 import dumps
-from cbor2.types import CBORTag
+from cbor2 import CBORTag, dumps
 from hashlib import sha1, sha256
 import argparse
 from io import StringIO
@@ -133,11 +132,11 @@ def addManifestEntry(filename, hashes, contents, entries):
     entries.append(entry)
 
 
-def getCert(subject, keyName, issuerName, ee, issuerKey="", validity=""):
+def getCert(subject, keyName, issuerName, ee, issuerKey="", validity="", tamperSpki=False):
     """Helper function to create an X509 cert from a specification.
     Takes the subject, the subject key name to use, the issuer name,
     a bool whether this is an EE cert or not, and optionally an issuer key
-    name."""
+    name, a validity period, and whether or not to tamper with the SPKI."""
     certSpecification = (
         "issuer:%s\n" % issuerName
         + "subject:"
@@ -156,13 +155,15 @@ def getCert(subject, keyName, issuerName, ee, issuerKey="", validity=""):
         certSpecification += "\nissuerKey:%s" % issuerKey
     if validity:
         certSpecification += "\nvalidity:%s" % validity
+    if tamperSpki:
+        certSpecification += "\ntamperSpki:"
     certSpecificationStream = StringIO()
     print(certSpecification, file=certSpecificationStream)
     certSpecificationStream.seek(0)
     return pycert.Certificate(certSpecificationStream)
 
 
-def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certValidity):
+def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certValidity, tamperSpki):
     """Given a COSE algorithm ('ES256', 'ES384', 'ES512'), an issuer
     name, the name of the issuer's key, and a validity period, returns a
     (algorithm id, pykey.ECCKey, encoded certificate) triplet for use
@@ -188,6 +189,7 @@ def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certVal
         True,
         issuerKey,
         certValidity,
+        tamperSpki,
     )
     return (algId, key, ee.toDER())
 
@@ -199,6 +201,7 @@ def signZip(
     rootName,
     rootKey,
     certValidity,
+    tamperSpki,
     manifestHashes,
     signatureHashes,
     pkcs7Hashes,
@@ -269,6 +272,7 @@ def signZip(
                     coseIssuerName,
                     issuerKey,
                     certValidity,
+                    tamperSpki,
                 )
                 for coseAlgorithm in coseAlgorithms
             ]
@@ -398,6 +402,12 @@ def main(outputFile, appPath, *args):
         + "algorithms (out of ES256, ES384, and ES512)",
         default=[],
     )
+    parser.add_argument(
+        "-t",
+        "--tamper-spki",
+        action="store_true",
+        help="Whether or not to tamper with the SPKI (only supported for COSE signatures)",
+    )
     parser.add_argument(
         "-z",
         "--pad-headers",
@@ -432,6 +442,7 @@ def main(outputFile, appPath, *args):
         parsed.root,
         parsed.root_key,
         parsed.cert_validity,
+        parsed.tamper_spki,
         [hashNameToFunctionAndIdentifier(h) for h in parsed.manifest_hash],
         [hashNameToFunctionAndIdentifier(h) for h in parsed.signature_hash],
         parsed.pkcs7_hash,
diff --git a/security/manager/ssl/tests/unit/test_signed_apps.js b/security/manager/ssl/tests/unit/test_signed_apps.js
index 1fe15b0bada..c370b6e9b31 100644
--- a/security/manager/ssl/tests/unit/test_signed_apps.js
+++ b/security/manager/ssl/tests/unit/test_signed_apps.js
@@ -534,6 +534,18 @@ add_signature_test(COSEOnly, function () {
   );
 });
 
+add_signature_test(COSEOnly, function () {
+  certdb.openSignedAppFileAsync(
+    Ci.nsIX509CertDB.AppXPCShellRoot,
+    original_app_path("tampered-spki"),
+    check_open_result(
+      "tampered-spki",
+      Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
+      []
+    )
+  );
+});
+
 // Sanity check to ensure a no-op tampering gives a valid result
 add_signature_test(PKCS7WithSHA1OrSHA256, function () {
   let tampered = tampered_app_path("identity_tampering");
diff --git a/security/manager/ssl/tests/unit/test_signed_apps/moz.build b/security/manager/ssl/tests/unit/test_signed_apps/moz.build
index 603549de778..5ad469c41fa 100644
--- a/security/manager/ssl/tests/unit/test_signed_apps/moz.build
+++ b/security/manager/ssl/tests/unit/test_signed_apps/moz.build
@@ -71,6 +71,7 @@ def SignedAppFile(name, flags, app_directory="app/"):
 # SignedAppFile('validity_expired.zip', ['-c', 'ES256', '-p', 'sha256', '--cert-validity', '19700101-19701212'])
 # SignedAppFile('validity_not_yet_valid.zip', ['-c', 'ES256', '-p', 'sha256', '--cert-validity', '99990101-99991212'])
 # SignedAppFile('alternate-root.zip', ['-k', 'alternate', '-c', 'ES256', '-p', 'sha256'])
+# SignedAppFile('tampered-spki.zip', ['-c', 'ES256', '-t'])
 
 # To generate a new entry, add SignedAppFile, run mach build and copy from
 # objdir/_tests/xpcshell/security/manager/ssl/tests/unit/test_signed_apps/
diff --git a/security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip b/security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip
new file mode 100644
index 00000000000..7155dc07cbe
Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip differ
diff --git a/security/manager/tools/pycert.py b/security/manager/tools/pycert.py
index c2be31d88d5..c4486279e1c 100755
--- a/security/manager/tools/pycert.py
+++ b/security/manager/tools/pycert.py
@@ -16,6 +16,7 @@ subject:<subject distinguished name specification>
 [validity:<YYYYMMDD-YYYYMMDD|duration in days>]
 [issuerKey:<key specification>]
 [subjectKey:<key specification>]
+[tamperSpki:]
 [signature:{sha256WithRSAEncryption,sha1WithRSAEncryption,
             md5WithRSAEncryption,ecdsaWithSHA256,ecdsaWithSHA384,
             ecdsaWithSHA512}]
@@ -391,6 +392,7 @@ class Certificate:
         self.savedEmbeddedSCTListData = None
         self.subjectKey = pykey.keyFromSpecification("default")
         self.issuerKey = pykey.keyFromSpecification("default")
+        self.tamperSpki = False
         self.serialNumber = None
         self.decodeParams(paramStream)
         # If a serial number wasn't specified, generate one based on
@@ -456,6 +458,8 @@ class Certificate:
             self.setupKey("issuer", value)
         elif param == "subjectKey":
             self.setupKey("subject", value)
+        elif param == "tamperSpki":
+            self.tamperSpki = True
         elif param == "signature":
             self.signature = value
         elif param == "serialNumber":
@@ -798,9 +802,20 @@ class Certificate:
         tbsCertificate["issuer"] = self.getIssuer()
         tbsCertificate["validity"] = self.getValidity()
         tbsCertificate["subject"] = self.getSubject()
-        tbsCertificate["subjectPublicKeyInfo"] = (
-            self.subjectKey.asSubjectPublicKeyInfo()
-        )
+        if self.tamperSpki:
+            algorithmIdentifier = rfc5280.AlgorithmIdentifier()
+            algorithmIdentifier["algorithm"] = univ.ObjectIdentifier(
+                "1.3.6.1.4.1.13769.666.666.666.1.500.9.1"
+            )
+            algorithmIdentifier["parameters"] = univ.Null()
+            spki = rfc5280.SubjectPublicKeyInfo()
+            spki["algorithm"] = algorithmIdentifier
+            spki["subjectPublicKey"] = univ.BitString("'0500'H")
+            tbsCertificate["subjectPublicKeyInfo"] = spki
+        else:
+            tbsCertificate["subjectPublicKeyInfo"] = (
+                self.subjectKey.asSubjectPublicKeyInfo()
+            )
         if self.extensions:
             extensions = rfc5280.Extensions().subtype(
                 explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/security/manager/ssl/tests/unit/sign_app.py b/security/manager/ssl/tests/unit/sign_app.py
index 66dec481008..599c344db1a 100755
--- a/security/manager/ssl/tests/unit/sign_app.py
+++ b/security/manager/ssl/tests/unit/sign_app.py
@@ -10,8 +10,7 @@ resulting zip file. Mainly for creating test inputs to the
 nsIX509CertDB.openSignedAppFileAsync API.
 """
 from base64 import b64encode
-from cbor2 import dumps
-from cbor2.types import CBORTag
+from cbor2 import CBORTag, dumps
 from hashlib import sha1, sha256
 import argparse
 from io import StringIO
@@ -133,11 +132,11 @@ def addManifestEntry(filename, hashes, contents, entries):
     entries.append(entry)
 
 
-def getCert(subject, keyName, issuerName, ee, issuerKey="", validity=""):
+def getCert(subject, keyName, issuerName, ee, issuerKey="", validity="", tamperSpki=False):
     """Helper function to create an X509 cert from a specification.
     Takes the subject, the subject key name to use, the issuer name,
     a bool whether this is an EE cert or not, and optionally an issuer key
-    name."""
+    name, a validity period, and whether or not to tamper with the SPKI."""
     certSpecification = (
         "issuer:%s\n" % issuerName
         + "subject:"
@@ -156,13 +155,15 @@ def getCert(subject, keyName, issuerName, ee, issuerKey="", validity=""):
         certSpecification += "\nissuerKey:%s" % issuerKey
     if validity:
         certSpecification += "\nvalidity:%s" % validity
+    if tamperSpki:
+        certSpecification += "\ntamperSpki:"
     certSpecificationStream = StringIO()
     print(certSpecification, file=certSpecificationStream)
     certSpecificationStream.seek(0)
     return pycert.Certificate(certSpecificationStream)
 
 
-def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certValidity):
+def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certValidity, tamperSpki):
     """Given a COSE algorithm ('ES256', 'ES384', 'ES512'), an issuer
     name, the name of the issuer's key, and a validity period, returns a
     (algorithm id, pykey.ECCKey, encoded certificate) triplet for use
@@ -188,6 +189,7 @@ def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certVal
         True,
         issuerKey,
         certValidity,
+        tamperSpki,
     )
     return (algId, key, ee.toDER())
 
@@ -199,6 +201,7 @@ def signZip(
     rootName,
     rootKey,
     certValidity,
+    tamperSpki,
     manifestHashes,
     signatureHashes,
     pkcs7Hashes,
@@ -269,6 +272,7 @@ def signZip(
                     coseIssuerName,
                     issuerKey,
                     certValidity,
+                    tamperSpki,
                 )
                 for coseAlgorithm in coseAlgorithms
             ]
@@ -398,6 +402,12 @@ def main(outputFile, appPath, *args):
         + "algorithms (out of ES256, ES384, and ES512)",
         default=[],
     )
+    parser.add_argument(
+        "-t",
+        "--tamper-spki",
+        action="store_true",
+        help="Whether or not to tamper with the SPKI (only supported for COSE signatures)",
+    )
     parser.add_argument(
         "-z",
         "--pad-headers",
@@ -432,6 +442,7 @@ def main(outputFile, appPath, *args):
         parsed.root,
         parsed.root_key,
         parsed.cert_validity,
+        parsed.tamper_spki,
         [hashNameToFunctionAndIdentifier(h) for h in parsed.manifest_hash],
         [hashNameToFunctionAndIdentifier(h) for h in parsed.signature_hash],
         parsed.pkcs7_hash,
diff --git a/security/manager/ssl/tests/unit/test_signed_apps.js b/security/manager/ssl/tests/unit/test_signed_apps.js
index 1fe15b0bada..c370b6e9b31 100644
--- a/security/manager/ssl/tests/unit/test_signed_apps.js
+++ b/security/manager/ssl/tests/unit/test_signed_apps.js
@@ -534,6 +534,18 @@ add_signature_test(COSEOnly, function () {
   );
 });
 
+add_signature_test(COSEOnly, function () {
+  certdb.openSignedAppFileAsync(
+    Ci.nsIX509CertDB.AppXPCShellRoot,
+    original_app_path("tampered-spki"),
+    check_open_result(
+      "tampered-spki",
+      Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
+      []
+    )
+  );
+});
+
 // Sanity check to ensure a no-op tampering gives a valid result
 add_signature_test(PKCS7WithSHA1OrSHA256, function () {
   let tampered = tampered_app_path("identity_tampering");
diff --git a/security/manager/ssl/tests/unit/test_signed_apps/moz.build b/security/manager/ssl/tests/unit/test_signed_apps/moz.build
index 603549de778..5ad469c41fa 100644
--- a/security/manager/ssl/tests/unit/test_signed_apps/moz.build
+++ b/security/manager/ssl/tests/unit/test_signed_apps/moz.build
@@ -71,6 +71,7 @@ def SignedAppFile(name, flags, app_directory="app/"):
 # SignedAppFile('validity_expired.zip', ['-c', 'ES256', '-p', 'sha256', '--cert-validity', '19700101-19701212'])
 # SignedAppFile('validity_not_yet_valid.zip', ['-c', 'ES256', '-p', 'sha256', '--cert-validity', '99990101-99991212'])
 # SignedAppFile('alternate-root.zip', ['-k', 'alternate', '-c', 'ES256', '-p', 'sha256'])
+# SignedAppFile('tampered-spki.zip', ['-c', 'ES256', '-t'])
 
 # To generate a new entry, add SignedAppFile, run mach build and copy from
 # objdir/_tests/xpcshell/security/manager/ssl/tests/unit/test_signed_apps/
diff --git a/security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip b/security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip
new file mode 100644
index 00000000000..7155dc07cbe
Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/tampered-spki.zip differ
Loading diff…