Skip to content

SECLEVEL set via ciphers option is applied too late in tls.createSecureContext #36655

Closed
@Hornwitser

Description

  • Version: v14.9.0
  • Platform: Linux box 5.8.5-arch1-1 #1 SMP PREEMPT Thu, 27 Aug 2020 18:53:02 +0000 x86_64 GNU/Linux
  • Subsystem: tls

What steps will reproduce the bug?

Creating secure context with a certificate who's key bits are too small for the default openssl SECLEVEL causes the operation to throw a key "too small error" even if the SECLEVEL is overridden to an acceptable value via the ciphers option. For example the following code throws on my system (note requires node-forge to generate the certificate, install it via npm):

const util = require("util");
const tls = require("tls");
const forge = require("node-forge");

async function test() {
    // Generate a TLS certificate with too few bits
    let keypair = forge.pki.rsa.generateKeyPair(512);
    let cert = forge.pki.createCertificate();
    cert.publicKey = keypair.publicKey;
    cert.serialNumber = "01";
    cert.validity.notBefore = new Date();
    cert.validity.notAfter = new Date(cert.validity.notBefore);
    cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
    let attrs = [{ name: "commonName", value: "localhost" }];
    cert.setSubject(attrs);
    cert.setIssuer(attrs);
    cert.sign(keypair.privateKey);
    let certPem = forge.pki.certificateToPem(cert);
    let keyPem = forge.pki.privateKeyToPem(keypair.privateKey);

    let serverOpts = {
        key: keyPem,
        cert: certPem,
        ciphers: "DEFAULT@SECLEVEL=0",
    }

    let clientOpts = {
        ciphers: "DEFAULT@SECLEVEL=0",
        ca: certPem,
    };

    // Create TLS server, this is the operation that fails.
    let server = tls.createServer(serverOpts, socket => {
        socket.on("data", (chunk) => {
            console.log("server pong");
            socket.end("pong");
        });
    });
    server.unref();
    await util.promisify(server.listen.bind(server))();

    // Try connecting to the server
    let addr = `https://localhost:${server.address().port}/`;
    let socket = tls.connect(server.address().port, "localhost", clientOpts, () => {
        socket.on("data", (chunk) => { });
        console.log("client ping");
        socket.write("ping");
    });
}

test().catch(console.log);

With the following error:

Error: error:140AB18F:SSL routines:SSL_CTX_use_certificate:ee key too small
    at Object.createSecureContext (_tls_common.js:129:17)
    at Server.setSecureContext (_tls_wrap.js:1323:27)
    at new Server (_tls_wrap.js:1181:8)
    at Object.createServer (_tls_wrap.js:1224:10)
    at test (bug.js:33:22)
    at Object.<anonymous> (bug.js:51:1)
    at Module._compile (internal/modules/cjs/loader.js:1075:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1096:10)
    at Module.load (internal/modules/cjs/loader.js:940:32)
    at Function.Module._load (internal/modules/cjs/loader.js:781:14) {
  library: 'SSL routines',
  function: 'SSL_CTX_use_certificate',
  reason: 'ee key too small',
  code: 'ERR_SSL_EE_KEY_TOO_SMALL'
}

(if this does not reproduce the issue it might be possible that the system default SECLEVEL is 0, in this case it's possible to reproduce the issue by adding the following to a .cnf file

openssl_conf = openssl_init

[openssl_init]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
CipherString = DEFAULT:@SECLEVEL=1

And then pointing node to it via the --openssl-config option.)

How often does it reproduce? Is there a required condition?

Whenever the default/configured SECLEVEL for openssl is greater than the one requested via the ciphers and this level is more strict than the certificate used requires.

What is the expected behavior?

Setting SECLEVEL via the ciphers option as Documented in the Ciphers List Format should allow a weaker certificate to be used than the system default SECLEVEL allows.

What do you see instead?

Node.js tries to add the certificate to the secure context before the ciphers option is process, which causes the default SECLEVEL to be used when evaluating the certificate. I know this to be the case as I tested reordering the certificate being added to the security context by using the following monkey patch:

const origCreateSecureContext = tls.createSecureContext;
tls.createSecureContext = function(options) {
    if (!options.cert || !options.key) {
        return origCreateSecureContext(options);
    }

    let lessOptions = { ...options };
    delete lessOptions.key;
    delete lessOptions.cert;
    let ctx = origCreateSecureContext(lessOptions);
    ctx.context.setCert(options.cert);
    ctx.context.setKey(options.key, undefined);
    return ctx;
};

If this is added to the reproduction code above it no longer throws a key too small error.

Additional information

Being able to set the security level in order to use certificates weaken than the default security setting allows is useful for automated testing. A viable workaround is to set the level through an openssl config file like shown above, but it's a bit of a sledgehammer approach. It looks like there's a SSL_CTX_set_security_level function to directly set the level instead of doing through the ciphers option, which might be a better alternative to setting it via the ciphers option.

Note that the reverse situation also does not work correctly: if SECLEVEL is increased in the ciphers option the certificate is still accepted, but handshakes with the server fails with Error: Client network socket disconnected before secure TLS connection was established.

Activity

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

Metadata

Assignees

No one assigned

    Labels

    tlsIssues and PRs related to the tls subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions