Skip to content

patching for edsca support since it is not in forge#206

Open
DonaldChung-HK wants to merge 2 commits into
masterfrom
fix-bypass-edsca-ssh-signiture-not-in-forge
Open

patching for edsca support since it is not in forge#206
DonaldChung-HK wants to merge 2 commits into
masterfrom
fix-bypass-edsca-ssh-signiture-not-in-forge

Conversation

@DonaldChung-HK

Copy link
Copy Markdown
Contributor

Description

This PR introduces a client-side polyfill to patch node-forge in-memory on the certificate download page.

The upcomming UK e-Science CA 3Bhas transitioned to signing certificates using ecdsa-with-SHA384 over the secp384r1 curve. Because node-forge natively only supports RSA signature validation layouts for X.509 structures, the request download on CA portal portal was throwing a fatal runtime exception (Unknown signature OID: 1.2.840.10045.4.3.3) inside certificateFromAsn1, blocking users from generating and downloading thei .p12 bundles.

The Fix

The injected script block hooks into the global forge instance immediately after loading from the CDN and applies the following overrides:

  1. Registers the missing OID mapping for 1.2.840.10045.4.3.3 to ecdsa-with-SHA384.
  2. **Proxies forge.pki.certificateFromAsn1** to dynamically feed a mock message digest configuration frame whenever an unrecognized ECDSA signature validation check is triggered. This forces the parser to complete successfully without dropping key fields.
  3. Dropping the prepended NULL before signature: An ASN1diff revealed that Forge's serialization layer was incorrectly appending a prim: NULL parameter to the ECDSA block during .p12 packaging (a assumption valid for RSA since the key length need to be specified, but illegal under ECDSA specs like RFC 5758). The patch recursively filters the abstract syntax tree right before compilation to strip this explicit NULL padding, ensuring that the downloaded certificate strictly matches the raw HSM bytes and successfully passes strict downstream openssl verify checks.

Security Evaluation

  • No Impact on Private Key Security: The polyfill strictly limits its operations to the public certificate parsing wrapper. The core cryptographic checks for private key decryption (forge.pki.decryptRsaPrivateKey) and the strict public/private key modulus verification (privateKey.n !== cert.publicKey.n) remain unchanged and fully enforced in the browser memory. A user still cannot extract or compile a bundle without providing the legitimate password and matching private key file.

Alternative

Ask user to paste the following Javascript block into console

(function() {
    // 1. Map the OID globally for SHA384
    forge.pki.oids['1.2.840.10045.4.3.3'] = 'ecdsa-with-SHA384';

    // 2. Wrap certificateFromAsn1 with a dynamic Proxy to bypass the internal validator crash
    const originalFromAsn1 = forge.pki.certificateFromAsn1;
    forge.pki.certificateFromAsn1 = function(obj, computeDigest) {
        try {
            return originalFromAsn1.call(this, obj, false);
        } catch (e) {
            if (e.message && e.message.includes("Unknown signature OID")) {
                console.warn(" Intercepted internal validator crash! Activating Proxy bypass...");
                
                const originalParams = forge.pki.x509.signatureParameters;
                const mockParam = {
                    name: 'ecdsa-with-SHA384',
                    getMessageDigest: function() { return forge.md.sha384.create(); }
                };
                
                // Force Forge to find a valid object for ANY OID it looks up during this frame
                forge.pki.x509.signatureParameters = new Proxy({}, {
                    get: function(target, prop) { return mockParam; }
                });

                try {
                    const cert = originalFromAsn1.call(this, obj, false);
                    forge.pki.x509.signatureParameters = originalParams;
                    return cert;
                } catch (retryError) {
                    forge.pki.x509.signatureParameters = originalParams;
                    throw retryError;
                }
            }
            throw e;
        }
    };

    // 3. Keep the safety net on the final digest function
    const nativeGetCertificateDigest = forge.pki.getCertificateDigest;
    forge.pki.getCertificateDigest = function(cert) {
        if (cert.signatureOid === '1.2.840.10045.4.3.3') {
            return forge.util.createBuffer();
        }
        return nativeGetCertificateDigest.apply(this, arguments);
    };

    // 4. Intercept the ASN.1 compilation to strip the illegal NULL parameter
    const originalToDer = forge.asn1.toDer;
    forge.asn1.toDer = function(obj) {
        const fixASN1 = function(node) {
            if (node && Array.isArray(node.value)) {
                const hasOid = node.value.some(child => 
                    child.type === forge.asn1.Type.OID && 
                    forge.asn1.derToOid(child.value) === '1.2.840.10045.4.3.3'
                );

                if (hasOid) {
                    const filtered = node.value.filter(child => child.type !== forge.asn1.Type.NULL);
                    if (filtered.length !== node.value.length) {
                        console.log("Stripped illegal ASN.1 NULL parameter from the output structure!");
                        node.value = filtered;
                    }
                }
                node.value.forEach(fixASN1);
            }
        };

        fixASN1(obj);
        return originalToDer.call(this, obj);
    };

    console.log("Completed");
})();

Type of Change

  • Bug fix


// 2. Intercept certificate parsing execution frames to handle missing OID validation
if (forge.pki.x509 && forge.pki.x509.signatureParameters) {
forge.pki.x509.signatureParameters['1.2.840.10045.4.3.3'] = {

@garaimanoj garaimanoj Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are using it multiple time, we can use

const OID = '1.2.840.10045.4.3.3';
const ecdsaSha384 = {
name: 'ecdsa-with-SHA384',
getMessageDigest: () => forge.md.sha384.create()
};

});
try {
const cert = originalFromAsn1.call(this, obj, false);
forge.pki.x509.signatureParameters = originalParams;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use
finally { forge.pki.x509.signatureParameters = originalParams; }

@DonaldChung-HK

Copy link
Copy Markdown
Contributor Author

Updated. Thanks.

@DonaldChung-HK

Copy link
Copy Markdown
Contributor Author

Verified, correctly downloaded certificate from 3B and 2B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants