Skip to content

tls: load NODE_EXTRA_CA_CERTS at startup#23354

Merged
refack merged 1 commit into
nodejs:masterfrom
oyyd:tls-cert
Oct 20, 2018
Merged

tls: load NODE_EXTRA_CA_CERTS at startup#23354
refack merged 1 commit into
nodejs:masterfrom
oyyd:tls-cert

Conversation

@oyyd

@oyyd oyyd commented Oct 9, 2018

Copy link
Copy Markdown
Contributor

NODE_EXTRA_CA_CERTS is not intended to be used to set the paths of extra certificates and this approach to setting is not reliable. This commit makes node load extra certificates at startup rather than on first use.

Fixes: #20434
Refs: #20432

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. tls Issues and PRs related to the tls subsystem. labels Oct 9, 2018

@sam-github sam-github left a comment

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.

This makes the env var behave as intended and described. One suggested change.

Comment thread doc/api/cli.md Outdated

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.

I find the text confusing here, I'm not sure what is meant by "setting paths". Rather than rewriting though, I suggest removing the entire doc change, both sentences. Its unnecessary, env variables are intended to be set outside node, not by node to configure itself at runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right. My explanation here makes the doc even confusing.

@oyyd

oyyd commented Oct 10, 2018

Copy link
Copy Markdown
Contributor Author

@sam-github @bnoordhuis PTAL

Comment thread lib/internal/tls.js Outdated

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.

One suggestion: I think this can be called unconditionally. certLoadExtraRootCertsFile is a null op if extra_root_certs_file hasn't been set, and it is only set if this env var is present, so this is a non-obvious optimization. I think its a bit fragile, if a CLI flag was introduced to set the extra_root_certs_file, for example, then this code would not execute, and the bug you are fixing would resurface for that use case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another potential footgun: the C++ code does suid root checks (calls SafeGetenv("NODE_EXTRA_CA_CERTS")); process.env does not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have removed the conditional statement as @sam-github suggested. After the removing, we don't use process.env.NODE_EXTRA_CA_CERTS in js land which avoids the potential footgun @bnoordhuis described.

Comment thread lib/internal/bootstrap/node.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This breaks ./configure --without-ssl builds. Not the require statement itself but the internalBinding('crypto') statement in the included file, it'll throw an exception.

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.

Would making this conditional on Boolean(process.versions.openssl) as test/common/util.js does for hasCrypto be OK, or is there a better way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have placed this line in if (process.versions.openssl) {.

... or is there a better way?

I think it's ok as lib/internal/util also uses process.versions.openssl to assert crypto:

const noCrypto = !process.versions.openssl;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

And a new test has been added to ensure that it won't throw without ssl.

Comment thread src/node_crypto.cc Outdated
Comment thread lib/internal/tls.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another potential footgun: the C++ code does suid root checks (calls SafeGetenv("NODE_EXTRA_CA_CERTS")); process.env does not.

@addaleax addaleax left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this a semver-major change? It seems like it could be?

Comment thread lib/internal/bootstrap/node.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could just access the binding directly here if you like

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I assumed there are codes other than just calling functions from binding before.

Accessing the binding directly is better as it could indicate why if (process.versions.openssl) is necessary to other contributors.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto about accessing the binding directly (again, only if you like)

@oyyd

oyyd commented Oct 11, 2018

Copy link
Copy Markdown
Contributor Author

@addaleax Yes. This might break something.

Comment thread lib/internal/tls.js Outdated

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.

Why the comma is introduced?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's unintentionally introduced(my personal code style :-)). I'm going to restore this line.

Comment thread src/node_crypto.cc Outdated

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.

Shouldn't this condition be split into two, like in the current code? If root_cert_store is already created and extra_root_certs_file is not empty, we should still add them, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If root_cert_store is already created and extra_root_certs_file is not empty, we should still add them, right?

No, it seems the original codes won't set root_cert_store again if it's available. Please remind me if I mistake something.

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.

You are correct. I misread the code. But I still have one question. As per old code if the root_cert_store is not there yet, a new instance of NewRootCertStore is created and assigned to it. Shouldn't the new code also do the same?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As per old code if the root_cert_store is not there yet, a new instance of NewRootCertStore is created and assigned to it.

Yes. One of the reasons is that I use root_cert_store to indicate whether the certs have been loaded in the flowing test.

But I think you are right. I'm going to modify the code and make the new code do the same.

@oyyd

oyyd commented Oct 11, 2018

Copy link
Copy Markdown
Contributor Author

@thefourtheye PTAL

@addaleax addaleax added the semver-major PRs that contain breaking changes and should be released in the next major version. label Oct 11, 2018

@thefourtheye thefourtheye left a comment

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.

Nit: The newly exposed functions' names sound a little wordy.

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.

Nit: Maybe we can just run this test only when crypto is not there, just to emphasize?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems that we don't run tests with --without-ssl build. I would like and change the comment a bit and keep the test.

@oyyd oyyd force-pushed the tls-cert branch 2 times, most recently from 17923f2 to 924e117 Compare October 15, 2018 03:13

@bnoordhuis bnoordhuis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left some comments but mostly LGTM.

Comment thread src/node_crypto.cc Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Style nit: 4 space indent (line continuation.)

Comment thread src/node_crypto.cc Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Style: root_cert_store == nullptr

(I know older code says !root_cert_store but new code shouldn't.)

Comment thread src/node_crypto.cc Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Style: can you line up the arguments?

Comment thread src/node_crypto.cc Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The certIsExtra... name is kind of misleading; the other cert... methods are for SPKAC.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

--expose-internals isn't necessary, is it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe clarify that this test is written for ./configure --without-ssl builds. (That's right, isn't it?

@oyyd oyyd force-pushed the tls-cert branch 2 times, most recently from 4a8bfbe to bcef787 Compare October 17, 2018 05:41
@oyyd

oyyd commented Oct 17, 2018

Copy link
Copy Markdown
Contributor Author

@bnoordhuis Thanks for your comments. PTAL.

Comment thread lib/internal/bootstrap/node.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could this possibly be injected via bootstrapper.cc instead? Especially since it's only used here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now, it's removed to UseExtraCaCerts.

@refack refack left a comment

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.

Left some questions, and requests.

Comment thread src/node_crypto.cc Outdated

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.

Avoid globals. If this is per environment, it should be a member property of node::Environment. If it's actually a per-process global, IMHO it should be grouped into a struct with root_cert_store and extra_root_certs_file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Grouping root_cert_store will make the code a bit messy. And as the extra_root_certs_file has been removed, I guess it's okay now.

Comment thread test/parallel/test-tls-env-extra-ca-file-load.js Outdated

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.

Since there is no test IMHO the description should be:

... won't throw even when there is no... Hence we don't test `common.hasCrypto`
                ^^^^

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I guess I made this test too confusing.

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.

IMHO this will be more readable is it was:

if {
} else if {
} else if {
} else {
  assert.fail("Bad child process invocation")
}

Having a if (process.argv[2] !== 'child') guard remove the need to do return in each clause

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.

This should be outside of the scope that start in L9

Comment thread src/node_crypto.cc Outdated

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.

What is the difference between this and the one before?
IF they can be merged, please do, else please add a comment.

Comment thread src/node_crypto.cc Outdated

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.

Is there a requirement to call this from JS? If not this should be called during Environment::Start, or even in

node/src/node.cc

Lines 2972 to 2975 in cf3f8dd

std::string extra_ca_certs;
if (SafeGetenv("NODE_EXTRA_CA_CERTS", &extra_ca_certs))
crypto::UseExtraCaCerts(extra_ca_certs);
}

https://github.com/nodejs/node/blob/bcef787dce0a380e3ce66936e72c49176de67838/src/node_crypto.cc#L835-L837

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Loading extra ca certs will emit a warning when the certs files are not valid:

ProcessEmitWarning(sc->env(),

And as the Environment and the js runtime(this name maybe not correct) is not setup yet while calling UseExtraCaCerts, we can't move the calling here.

I would like to move LoadExtraRootCertsFile() to bootstrapper.cc as @jasnell suggested, how do you think about it?

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.

Since IIUC this is sort of a command line error, we do have precident of printing the error directly:

node/src/node.cc

Lines 2434 to 2444 in cf3f8dd

if (!errors.empty()) {
for (auto const& error : errors) {
fprintf(stderr, "%s: %s\n", args->at(0).c_str(), error.c_str());
}
exit(9);
}
if (per_process_opts->print_version) {
printf("%s\n", NODE_VERSION);
exit(0);
}

node/src/node.cc

Lines 354 to 359 in cf3f8dd

void StartTracingAgent() {
if (!trace_enabled_categories.empty()) {
fprintf(stderr, "Node compiled with NODE_USE_V8_PLATFORM=0, "
"so event tracing is not available.\n");
}
}

node/src/node.cc

Lines 382 to 415 in cf3f8dd

void PrintErrorString(const char* format, ...) {
va_list ap;
va_start(ap, format);
#ifdef _WIN32
HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE);
// Check if stderr is something other than a tty/console
if (stderr_handle == INVALID_HANDLE_VALUE ||
stderr_handle == nullptr ||
uv_guess_handle(_fileno(stderr)) != UV_TTY) {
vfprintf(stderr, format, ap);
va_end(ap);
return;
}
// Fill in any placeholders
int n = _vscprintf(format, ap);
std::vector<char> out(n + 1);
vsprintf(out.data(), format, ap);
// Get required wide buffer size
n = MultiByteToWideChar(CP_UTF8, 0, out.data(), -1, nullptr, 0);
std::vector<wchar_t> wbuf(n);
MultiByteToWideChar(CP_UTF8, 0, out.data(), -1, wbuf.data(), n);
// Don't include the null character in the output
CHECK_GT(n, 0);
WriteConsoleW(stderr_handle, wbuf.data(), n - 1, nullptr, nullptr);
#else
vfprintf(stderr, format, ap);
#endif
va_end(ap);
}


Alternatively if you add this to Environment::Start, you could use this to setup the warning:
for example after:

node/src/env.cc

Line 278 in cf3f8dd

SetupProcessObject(this, args, exec_args);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we do have precident of printing the error directly

Then I think moving the calling into the function body of UseExtraCaCerts is better. Let me take a try on my mac(as I remember there is a test to check the warning message.).

@refack

refack commented Oct 17, 2018

Copy link
Copy Markdown
Contributor

Hello @oyyd, thank you for this fix.
I left some questions, and points that might need changes. I don't have the full context of the review up till now, so if I'm suggesting something that negates a previous comment, sorry.

Comment thread src/node_crypto.cc Outdated
@oyyd

oyyd commented Oct 18, 2018

Copy link
Copy Markdown
Contributor Author

Hi @refack, feel free to leave more comments :-). I'm going to check them later.

Comment thread test/parallel/test-tls-env-extra-ca-no-crypto.js Outdated
Comment thread src/node_crypto.cc Outdated

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.

Can we replace this new global boolean with following logic:

  if (!extra_root_certs_file.empty()) {
    ...
    if (err) {
      ...
      extra_root_certs_file = nullptr;
    }
  }
...
  static void IsExtraRootCertsFileLoaded(const FunctionCallbackInfo<Value>& args) {
    return args.GetReturnValue().Set(extra_root_certs_file != nullptr);
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The compiler complained about the extra_root_certs_file != nullptr. (Otherwise, I think it should be a better choice.)

@oyyd

oyyd commented Oct 19, 2018

Copy link
Copy Markdown
Contributor Author

@refack Thanks. Please take another look.

@richardlau

Copy link
Copy Markdown
Member

The new test needs to extend env rather than replace it for the child processes otherwise it is clearing LD_LIBRARY_PATH which is why the shared libraries are failing to load.

@refack

refack commented Oct 20, 2018

Copy link
Copy Markdown
Contributor

The new test needs to extend env rather than replace it for the child processes otherwise it is clearing LD_LIBRARY_PATH which is why the shared libraries are failing to load.

Ohh, that's a common pitfall.

Comment thread test/parallel/test-tls-env-extra-ca-file-load.js Outdated

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.

Suggested change
{ env: { NODE_EXTRA_CA_CERTS } },
{env: Object.assign({}, process.env, { NODE_EXTRA_CA_CERTS })},

@refack

refack commented Oct 20, 2018

Copy link
Copy Markdown
Contributor

@oyyd

oyyd commented Oct 20, 2018

Copy link
Copy Markdown
Contributor Author

The failed travis ci test is unrelated. Now it's ready.

This commit makes node load extra certificates at startup instead
of first use.

PR-URL: nodejs#23354
Fixes: nodejs#20434
Refs: nodejs#20432
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. semver-major PRs that contain breaking changes and should be released in the next major version. tls Issues and PRs related to the tls subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

src: load NODE_EXTRA_CA_CERTS at startup

9 participants