derhuerst / gemini

Gemini protocol server & client for Node.js.
ISC License
48 stars 8 forks source link

how to handle self-signed certificates? #14

Open matdombrock opened 1 year ago

matdombrock commented 1 year ago

When I try to run a basic client example I get this error:

Error: self-signed certificate
    at TLSSocket.onConnectSecure (node:_tls_wrap:1540:34)
    at TLSSocket.emit (node:events:513:28)
    at TLSSocket._finishInit (node:_tls_wrap:959:8)
    at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:743:12) {
  code: 'DEPTH_ZERO_SELF_SIGNED_CERT'
}

Full client.js

const request = require('@derhuerst/gemini/client')

const opt = {
    // follow redirects automatically
    // Can also be a function `(nrOfRedirects, response) => boolean`.
    followRedirects: false,
    // client certificates
    useClientCerts: false,
    letUserConfirmClientCertUsage: null,
    //clientCertStore: defaultClientCertStore,
    // time to wait for socket connection & TLS handshake
    connectTimeout: 60 * 1000, // 60s
    // time to wait for response headers *after* the socket is connected
    headersTimeout: 30 * 1000, // 30s
    // time to wait for the first byte of the response body *after* the socket is connected
    timeout: 40 * 1000, // 40s
    // additional options to be passed into `tls.connect`
    tlsOpt: {},
    // verify the ALPN ID chosen by the server
    // see https://de.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation
    verifyAlpnId: alpnId => alpnId ? (alpnId === ALPN_ID) : true,
};

request('gemini://gemini.circumlunar.space/', opt, (err, res) => {
    console.log(opt);
    if (err) {
        console.error(err)
        process.exit(1)
    }

    console.log(res.statusCode, res.statusMessage)
    if (res.meta) console.log(res.meta)
    res.pipe(process.stdout)
})

Environment

# Node
v18.16.0
# NPM
v9.6.5

Resolution

I was able to get requests working by adding the following line to the top of client.js.

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

I'm not totally sure why this fixes it because I don't understand the root problem. But this works on my end.

Source for solution: https://stackoverflow.com/questions/23601989/client-certificate-validation-on-server-side-depth-zero-self-signed-cert-error

derhuerst commented 1 year ago

It seems like gemini://gemini.circumlunar.space/ uses a self-signed SSL certificate. By default – and @derhuerst/gemini does not change these defaults – Node.js rejects certificates that are not tied to one from its list of CA certificates.

This behaviour can be changed with tls.connects()'s rejectUnauthorized: false option; It can be passed into @derhuerst/gemini as tlsOpt: {rejectUnauthorized: false}. This will effectively allow any certificate, so it exposes you to MITM attacks.

The proper way to solve this is to use tls.connects()'s checkServerIdentity() option in order to store the certificate's footprint and then later compare it (TOFU).

derhuerst commented 1 year ago
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

I'm not totally sure why this fixes it because I don't understand the root problem. But this works on my end.

While your solution does allow any certificate for all Gemini servers (which is insecure, see above), it also disables all certificate checks in all other encrypted network communication that your Node.js script does (DB connection, calls to 3rd-party APIs, etc)!

derhuerst commented 1 year ago

Does that answer your question?

matdombrock commented 1 year ago

From what I understand, self signed certs are the norm for most Gemini capsules.

The proper way to solve this is to use tls.connects()'s checkServerIdentity() option in order to store the certificate's footprint and then later compare it (TOFU).

It might make sense for that to be the default behavior if possible. Or at least add an example of this use case.

derhuerst commented 1 year ago

From what I understand, self signed certs are the norm for most Gemini capsules.

Huh, still it feels weird to turn off the cert validation by default. Without another reasonably effective way to know some host's cert or at least TOFU, this is almost like dropping all encryption, as it allows anyone to MITM the connection.

The proper way […] to store the certificate's footprint and then later compare it (TOFU).

It might make sense for that to be the default behavior if possible. Or at least add an example of this use case.

I agree, this should be documented; Having an example which demonstrates how to implement server cert TOFU using e.g. plain text files or an SQLite DB would be great. PR welcome!

Given that @derhuerst/gemini is intended to be a low-level und environment-agnostic implementation of the protocol, an actual implementation should IMO be in a library on top of it, e.g. gemini-fetch. @RangerMauve What do you think?

derhuerst commented 4 months ago

@matdombrock Would you mind submitting a PR?