current commit
260916.1 · lib,src: apply multiple updates to dtls implementation
// Flags: --experimental-dtls --no-warnings

// Test: the requestCert / rejectUnauthorized matrix on a DTLS server.
//
//   requestCert: false            -> no certificate is requested
//   requestCert, rejectUnauthorized -> OpenSSL enforces, peer gets an alert
//   requestCert, !rejectUnauthorized -> certificate is requested, the
//                                       handshake completes either way, and
//                                       the application decides using
//                                       session.authorized
//
// The last row is the node:tls idiom for "ask for a certificate and let the
// application decide". It previously mapped to SSL_VERIFY_NONE, so no
// CertificateRequest was sent at all and the server saw no peer certificate
// even when the client offered a valid one.

import { hasCrypto, mustNotCall, skip } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';

if (!hasCrypto) {
  skip('missing crypto');
}

if (!process.features.dtls) {
  skip('DTLS is not enabled');
}

const { connect, listen } = await import('node:dtls');

const cert = fixtures.readKey('agent1-cert.pem');
const key = fixtures.readKey('agent1-key.pem');
const ca1 = fixtures.readKey('ca1-cert.pem');
const ca2 = fixtures.readKey('ca2-cert.pem');

// Runs one handshake and reports what the server made of the client.
async function handshake(serverOptions, clientOptions) {
  const gotSession = Promise.withResolvers();
  const server = listen((session) => {
    session.onhandshake = () => gotSession.resolve(session);
  }, { cert, key, host: '127.0.0.1', port: 0, ...serverOptions });

  const client = connect('127.0.0.1', server.address.port, {
    servername: 'agent1', ca: [ca1], ...clientOptions,
  });

  let result;
  try {
    await client.opened;
    const session = await gotSession.promise;
    result = {
      rejected: false,
      sawPeerCertificate: session.peerCertificate !== undefined,
      authorized: session.authorized,
      authorizationError: session.authorizationError,
    };
  } catch (err) {
    result = { rejected: true, message: err.message };
  }

  try {
    await client.close();
  } catch {
    // The handshake may already have torn the session down.
  }
  await server.close();
  return result;
}

// --- requestCert with rejectUnauthorized disabled ------------------------

// A trusted client certificate is actually requested and received. This is
// the case that silently produced no certificate at all before.
assert.deepStrictEqual(
  await handshake({ requestCert: true, rejectUnauthorized: false, ca: [ca1] },
                  { cert, key }),
  {
    rejected: false,
    sawPeerCertificate: true,
    authorized: true,
    authorizationError: undefined,
  });

// An untrusted client certificate is received, and the handshake completes so
// the application can decide.
assert.deepStrictEqual(
  await handshake({ requestCert: true, rejectUnauthorized: false, ca: [ca2] },
                  { cert, key }),
  {
    rejected: false,
    sawPeerCertificate: true,
    authorized: false,
    authorizationError: 'SELF_SIGNED_CERT_IN_CHAIN',
  });

// A client that declines to send one is tolerated rather than rejected.
assert.deepStrictEqual(
  await handshake({ requestCert: true, rejectUnauthorized: false, ca: [ca1] },
                  {}),
  {
    rejected: false,
    sawPeerCertificate: false,
    authorized: false,
    authorizationError: 'UNABLE_TO_GET_ISSUER_CERT',
  });

// --- requestCert, enforced -----------------------------------------------

// OpenSSL rejects these, so the peer receives a real alert rather than having
// the session quietly dropped after the handshake.
{
  const noCert = await handshake({ requestCert: true, ca: [ca1] }, {});
  assert.strictEqual(noCert.rejected, true);
  assert.match(noCert.message, /handshake failure/);

  const untrusted = await handshake({ requestCert: true, ca: [ca2] },
                                    { cert, key });
  assert.strictEqual(untrusted.rejected, true);
  assert.match(untrusted.message, /unknown ca/);
}

// --- no requestCert ------------------------------------------------------

// rejectUnauthorized alone must not turn a server into one that demands
// client certificates.
assert.deepStrictEqual(
  await handshake({ rejectUnauthorized: true, ca: [ca1] }, { cert, key }),
  {
    rejected: false,
    sawPeerCertificate: false,
    authorized: false,
    authorizationError: 'UNABLE_TO_GET_ISSUER_CERT',
  });

// --- validation ----------------------------------------------------------

assert.throws(
  () => listen(mustNotCall(), {
    cert, key, host: '127.0.0.1', port: 0, requestCert: 'yes',
  }),
  { code: 'ERR_INVALID_ARG_TYPE' });