Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: Add crypto.getEngines() #10865

Closed
wants to merge 1 commit into from
Closed

Conversation

shigeki
Copy link
Contributor

@shigeki shigeki commented Jan 18, 2017

This adds a new api to show the list of loaded engines of OpenSSL.
It also includes the test of dynamic engine for crypto.setEngine() and fixes missed test coverages as discussed in #10786.

A test engine is built from test/fixtures/openssl_test_engine/ by adding a new target in node.gyp, but fipsld seems to have an error with linking the test engine with libcrypto.a so that a dynamic engine test is skipped in FIPS mode.

I've already made several CI tests and found that tests on Windows and arm were failed because test engine files were lost in CI environment. They can be solved by includingRelease\node_test_engine.dll in Windows tests and out/Release/libnode_test_engine.so in arm tests in binary/binary.tar.gz and binary/binary.tar.xz . I would like ask someone in @nodejs/build to help it.

In my local environments, tests in Win and arm are fine.

CC @nodejs/crypto and @jasnell

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
Affected core subsystem(s)

crypto

@shigeki shigeki added crypto Issues and PRs related to the crypto subsystem. semver-minor PRs that contain new features and should be released in the next minor version. labels Jan 18, 2017
@nodejs-github-bot nodejs-github-bot added build Issues and PRs related to build files or the CI. c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. lts-watch-v6.x labels Jan 18, 2017
@shigeki
Copy link
Contributor Author

shigeki commented Jan 18, 2017

CI is https://ci.nodejs.org/job/node-test-pull-request/5921/ . Tests in Win and arm would be failed.

console.log(crypto.getEngines());
// Prints:
// [ { id: 'rdrand', name: 'Intel RDRAND engine' },
// { id: 'dynamic', name: 'Dynamic engine loading support' } ]
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor nit: this line looks better indented two more spaces to the right.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will fix it, thanks.

lib/crypto.js Outdated
@@ -633,6 +633,11 @@ exports.setEngine = function setEngine(id, flags) {
return binding.setEngine(id, flags);
};


exports.getEngines = function getEngines() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we get away with directly assigning to binding.getEngines ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean we should use internalUtil.cachedResult such as getCipher() ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant if we could just do exports.getEngines = binding.getEngines; and avoid the wrapper function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fine with me.

node.gyp Outdated
@@ -947,6 +947,18 @@
],
}],
]
},
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to put this whole definition in a separate gyp file in test/ somewhere instead of node.gyp, since it's only used for a test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think having target_name and type is good to know its purpose otherwise we need to write a comment of it nearly as the same description.

@mscdex
Copy link
Contributor

mscdex commented Jan 18, 2017

Perhaps it might be useful to also have a getEngine() that returns the currently set engine (kind of like the get/set for FIPS mode)?

@shigeki
Copy link
Contributor Author

shigeki commented Jan 18, 2017

The test was unstable. Here is a new test result, https://ci.nodejs.org/job/node-test-commit/7335/

'use strict';
const common = require('../common');

if (!common.hasCrypto) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

found++;
});

assert.strictEqual(found, 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

assert conflicts with comment about "at least one"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The description of comment was wrong. Fixed.

let found = 0;

// check at least one builtin engine exists.
crypto.getEngines().forEach((e) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

same as found = array.filter((e) => e.id === 'dynamic').length?


crypto.setEngine(test_engine);

crypto.getEngines().forEach((e) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

found = crypto.getEngines.filter((e) => e.id === test_engine_id && e.name === test_engine_name)).length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there any reason to use filter rather than forEach? It seems to be equivalent for me.

Copy link
Contributor

Choose a reason for hiding this comment

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

Its true that many of the Array methods can be implemented in terms of forEach(), so they are equivalent in that sense. Someone has to read this code, and understand that it is filtering. Better to just call filter(). And if you don't, @Trott or one of the code-and-learn people may come through and modify this to use es6 and builtin library functions - better avoid the churn and do it now, I think.

fs.accessSync(test_engine);
}, 'node test engine ' + test_engine + ' is not found.');

crypto.setEngine(test_engine);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe call twice to prove its possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is possible. Do we need a such kind of combination tests for setEngine?

Copy link
Contributor

@sam-github sam-github Jan 19, 2017

Choose a reason for hiding this comment

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

Tests should test invalid behaviours, and obscure corner conditions. If it is allowed to call multiple times, the test should do, and assert on the result, IMO. It would be possible that the second call always fails, for example, or someone might believe that both "RSA" engines get added (which isn't true). The docs don't actually say what happens when setEngine() is called multiple times with the same flag. The latest call will replace the previous default, but its not stated.


for (e = ENGINE_get_first(); e != nullptr; e = ENGINE_get_next(e)) {
const char* id = ENGINE_get_id(e);
const char* name = ENGINE_get_name(e);
Copy link
Contributor

Choose a reason for hiding this comment

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

too bad there is no ENGINE_get_flags() so we can know the flags

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. SetEngine did not have ENGINE_set_flag so that flags was always 0 for dynamic engines. Fixed and add a new flags property.

@sam-github
Copy link
Contributor

I thought there was not one engine, but a set of engines available at a time, and which one is used depends on the operation, so a getEngine() for current wouldn't make sense. @mscdex Am I misunderstanding? I've not worked much with engines.

@mscdex
Copy link
Contributor

mscdex commented Jan 18, 2017

@sam-github I'm not sure I follow. If there is a setEngine(), I would think that a getEngine() would be useful to allow one to know which engine is currently set (and especially useful in tests to make sure an engine is actually set)?

@sam-github
Copy link
Contributor

I think getEngines() may need another name. Perhaps just engines()? or listEngines()?

@mscdex I'm just looking at the code, and going off of how I would expect ossl to workcrypto.setEngine() is basically a call to https://github.com/nodejs/node/blob/master/deps/openssl/openssl/crypto/engine/eng_fat.c#L64

You can see that what it is doing is that for specific crypto algs, named by bits a field, an engine is then set as a default for that specific alg. So you can call setEngine() with X for RSA, and B for ECDH, and now you have two default engines. So, what would getEngine() return? It would have to be implemented as getEngine(flag), returning the default engine for a specific alg, and it appears such functions actually exist:

https://github.com/nodejs/node/blob/master/deps/openssl/openssl/crypto/engine/engine.h#L679-L685

Using ENGINE_METHOD_ALL or none would have to be disallowed, and it looks to me like some valid set_default types lack the corresponding get function.

@shigeki
Copy link
Contributor Author

shigeki commented Jan 19, 2017

At first, I put a name of crypto.getEngineList() but there are not existing API named get*List() so I changed it.

@jasnell
Copy link
Member

jasnell commented Mar 24, 2017

Ping @sam-github @shigeki ... updates on this one?

@jasnell jasnell added the stalled Issues and PRs that are stalled. label Mar 24, 2017
@shigeki
Copy link
Contributor Author

shigeki commented Mar 25, 2017

CI was fixed to include an engine module. I will make rebase this and resolve conflicts later today.

@sam-github @mscdex We are still in a discussion of the name of this API. I do not have any strong opinion on it. I will take it if both of you can agree.

This adds a new api to show the list of loaded engines of OpenSSL.
It also includes the test of dynamic engine for `crypto.setEngine()`.
@gibfahn
Copy link
Member

gibfahn commented Mar 27, 2017

@shigeki looks like linuxone failed with:

 g++ -shared -pthread -rdynamic -m64 -march=z196 -lopenssl -L/data/iojs/build/workspace/node-test-commit-linuxone/nodes/rhel72-s390x/out/Release/obj.target/deps/openssl  -Wl,-soname=libnode_test_engine.so -o /data/iojs/build/workspace/node-test-commit-linuxone/nodes/rhel72-s390x/out/Release/obj.target/libnode_test_engine.so -Wl,--start-group /data/iojs/build/workspace/node-test-commit-linuxone/nodes/rhel72-s390x/out/Release/obj.target/node_test_engine/test/fixtures/openssl_test_engine/node_test_engine.o -Wl,--end-group 
s390x/out/Release/obj.target/mkpeephole/deps/v8/src/interpreter/mkpeephole.o.d.raw   -c -o /data/iojs/build/workspace/node-test-commit-linuxone/nodes/rhel72-s390x/out/Release/obj.target/mkpeephole/deps/v8/src/interpreter/mkpeephole.o ../deps/v8/src/interpreter/mkpeephole.cc
cannot find -lopenssl
collect2: error: ld returned 1 exit status
make[2]: *** [/data/iojs/build/workspace/node-test-commit-linuxone/nodes/rhel72-s390x/out/Release/obj.target/libnode_test_engine.so] Error 1
make[2]: *** Waiting for unfinished jobs....

Is this something you expect, or a linuxone specific issue? If the latter, we can get someone to take a look at it.

@shigeki
Copy link
Contributor Author

shigeki commented Mar 27, 2017

@gibfahn Yes, I found that a build errors were occurred on some platforms(e.g. lineone, freebsd, 64bit smartos). They seems to be caused by build dependencies for the test engine was built before libopenssl.a. This did not have this error before, I will investigate it.

@gibfahn
Copy link
Member

gibfahn commented Mar 27, 2017

cc/ @nodejs/platform-s390 @joransiu @jBarz @jbajwa FYI ^

@fhinkel
Copy link
Member

fhinkel commented May 26, 2017

@shigeki Looks like this has been sitting here for quite a while. Sorry. Can you rebase so we can ran the CI?

@sam-github Have your comments been addressed?

@sam-github
Copy link
Contributor

Its stalled, we are still not sure what to name the API, I think.

Copy link
Member

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

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

LGTM modulo style nits, although the build issues probably still need to be worked out.

FWIW, crypto.getEngines() seems like a fine name to me.

#include <openssl/engine.h>

static const char *engine_id = "node_test_engine";
static const char *engine_name = "Node Test Engine for OpenSSL";
Copy link
Member

Choose a reason for hiding this comment

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

Tiny style nit: star leans left (const char* engine_id = ....). You could also writes this as:

static const char engine_id[] = ...;

Saves a pointer indirection.

'sources': ['node_test_engine.c'],
'conditions': [
['OS=="mac"', {
'include_dirs': ['<(PRODUCT_DIR)/../../deps/openssl/openssl/include',],
Copy link
Member

Choose a reason for hiding this comment

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

Superfluous comma, also in the other include_dirs lists.

engine_lib = 'node_test_engine.dll';
} else if (common.isAix) {
engine_lib = 'libnode_test_engine.a';
} else if (process.platform === 'darwin') {
Copy link
Member

Choose a reason for hiding this comment

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

common.isOSX


const test_engine_id = 'node_test_engine';
const test_engine_name = 'Node Test Engine for OpenSSL';
const test_engine = path.join(path.dirname(process.execPath), engine_lib);
Copy link
Member

Choose a reason for hiding this comment

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

Tiny style nit: JS code uses camelCase, not snake_case (for the most part.)


assert.strictEqual(found, 1);

// check set and get node test engine of
Copy link
Member

Choose a reason for hiding this comment

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

s/check/Check/


Returns an array of objects with id, name and flags of loaded engines.
The flags value represents an integer of one of or a mix of the crypto
constants described above.
Copy link
Contributor

Choose a reason for hiding this comment

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

docs can move around, better to link to crypto.setEngine() explicitly


if (id == nullptr || name == nullptr) {
ENGINE_free(e);
return env->ThrowError("Invalid Engine: id or name is not found.");
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this is the right approach. If there is an engine with an id, but no name, then this will throw, but the user won't know what engine it is that has a name, it could be very frustrating. What about just using "" for a missing id or name, as a replacment for NULL? Or would that be terrible? I'm not sure how common it is that engines don't have this data, I would have hoped OpenSSL did not allow invalid engines!

fs.accessSync(test_engine);
}, 'node test engine ' + test_engine + ' is not found.');

crypto.setEngine(test_engine);
Copy link
Contributor

@sam-github sam-github Jan 19, 2017

Choose a reason for hiding this comment

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

Tests should test invalid behaviours, and obscure corner conditions. If it is allowed to call multiple times, the test should do, and assert on the result, IMO. It would be possible that the second call always fails, for example, or someone might believe that both "RSA" engines get added (which isn't true). The docs don't actually say what happens when setEngine() is called multiple times with the same flag. The latest call will replace the previous default, but its not stated.

crypto.getEngines().forEach((e) => {
if (e.id === 'dynamic')
found++;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

by reimplementing filter() and ()=> with forEach(), this code is 5 lines instead of 1.

Copy link
Member

Choose a reason for hiding this comment

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

You mean

found += crypto.getEngines().filter(e => e.id === 'dynamic').length

?


crypto.setEngine(test_engine);

crypto.getEngines().forEach((e) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Its true that many of the Array methods can be implemented in terms of forEach(), so they are equivalent in that sense. Someone has to read this code, and understand that it is filtering. Better to just call filter(). And if you don't, @Trott or one of the code-and-learn people may come through and modify this to use es6 and builtin library functions - better avoid the churn and do it now, I think.

@BridgeAR
Copy link
Member

Should this be closed or is this something that is still worked on?
If the latter, it would need a rebase.

@BridgeAR
Copy link
Member

BridgeAR commented Sep 8, 2017

Ping @shigeki

@BridgeAR
Copy link
Member

Ping @shigeki again

@BridgeAR
Copy link
Member

Or after looking at this again - I am actually going to close this. There was no response since March.

@shigeki if you would like to pursue this further, please reopen.

@BridgeAR BridgeAR closed this Sep 23, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
build Issues and PRs related to build files or the CI. c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. semver-minor PRs that contain new features and should be released in the next minor version. stalled Issues and PRs that are stalled.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants