-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
feat(op_crates/crypto): Introduce Web Crypto API #8984
Conversation
This PR is very WIP. I opened it so that you can provide guidance early enough. |
@yacinehmito FYI I have started implementing WPT in #8990. That might be helpful for this PR. Regarding which crypto lib to use: lets use |
I updated the PR description to account for the decisions that have been made and the changes in approach, and added a few more information on:
|
I pushed a rough implementation of digest using ring. Also, I wonder if it wouldn't be preferable to put the |
core/zero_copy_buf.rs
Outdated
// TODO: Determine whether it's worth exposing just to assert the size | ||
pub byte_length: usize, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is supposed to be private, but I needed it to check that the size of the provided buffer is the expected one.
I don't think exposing that data is harmful. Let me know what you prefer.
If we want to keep this field private, we can also manually pass the size of the instantiated buffer as part of Value
. It's a bit more work but it's doable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert, you don't need to change ZeroCopyBuf
to check length; ZeroCopyBuf
derefs to [u8]
and you can use its size()
to check it.
// Algorithm normalization, which involves storing the expected data for | ||
// each pair of algorithm and crypto operation, is done on the JS side because | ||
// it is convenient. | ||
// Shall it stay that way, or are we better to move it on the Rust side? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think?
op_crates/crypto/01_crypto.js
Outdated
const supportedAlgorithms = { | ||
digest: new RegisteredAlgorithmsContainer({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This structure follows the spec. It mentions that implementations need a container called supportedAlgorithms
that maps operation names to registeredAlgorithms
, which in turn maps algorithm names to the expected shape of the algorithm input parameters.
op_crates/crypto/01_crypto.js
Outdated
const digestByteLengths = { | ||
"SHA-1": 20, | ||
"SHA-256": 32, | ||
"SHA-384": 48, | ||
"SHA-512": 64, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that we need to repeat that information on the JS side. ring
already knows about this.
I opted for copying it here and asserting that the size of the instantiated buffer matches the one expected by ring
.
(This is why I exposed ZeroCopyBuf.byte_length
earlier, although it is not strictly necessary)
I see two other options to avoid this:
- Instantiate the
ArrayBuffer
on the Rust side. IMHO it is preferable, but would require significant work. - Expose an OP to fetch the sizes defined on the Rust side. It's not as good in terms of guarantees, but at least we don't have to hardcode the lengths like that.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are fixed values that will not change in the future (defined by spec), so it is fine to hard code them. Sending over the information in an extra op adds too much complexity and overhead.
"SHA-512": 64, | ||
}; | ||
|
||
function normalizeAlgorithm(algorithm, registeredAlgorithms) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing all that work in JS because:
- It's easier
- We only ever have to serialise algorithm parameters that are normalised, meaning we will never attempt to serialise the input as provided by the user (which could be huge, be of a weird type, or contain circular references)
op_crates/crypto/lib.rs
Outdated
) -> Result<Value, AnyError> { | ||
let mut zc = zero_copy; | ||
|
||
// FIXME: This chain of `unwrap` is meh. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it deemed not clean enough, and if so how would you fix that?
Because normalisation has already happened on the JS side, unwrap()
should never panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users can still send garbage data manually, you can use something like let args: SubtleDigestArgs = serde_json::from_value(args)?;
like it's done in other ops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users can still send garbage data manually
How? Can they call an OP directly without going through the JS API?
Regardless, I'll implement your suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can they call an OP directly without going through the JS API?
Yes. This is not covered under any stability guarantee though, and is not officially documented. We should try minimize rust panics where possible and gracefully return errors to JS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How? Can they call an OP directly without going through the JS API?
They can using JS API:
Deno.core.dispatch("op_crypto", new TextEncoder().encode("some garbage data that is not valid json"));
op_crates/crypto/lib.rs
Outdated
// TODO: This should run in another thread | ||
// Otherwise, even though the API is async, it is blocking the main thread | ||
// See https://stackoverflow.com/questions/61292425/how-to-run-an-asynchronous-task-from-a-non-main-thread-in-tokio |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understand, jsonOpAsync()
doesn't necessarily mean that the code runs asynchronously. It just means that it returns a Promise
(a Future
on Rust side).
So, to make it truly asynchronous, we'd have to run the call to ring's digest()
in another thread. I have no idea how to do this. Is there an example of this already in the codebase?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no point in making this op async if the Rust API is sync; just use sync op and if you need to return promise do it manually in JS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have some synchronous CPU intensive operation with an async API in JS, you can run it in the tokio blocking thread pool (tokio::task::spawn_blocking).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lucacasonato Thank you very much, it's exactly what I was looking for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very good so far. Regarding your question about merging this in smaller PRs: yes please. It makes reviewing a lot easier. I don't think there is a need to put this behind unstable, because there will be no interface changes in the future as the interface is defined by the WebCrypto spec. We don't consider bug fixes (or alignment with browser) in Web APIs breaking changes.
If you think it makes sense to put this behind --unstable though, we can do that too. @bartlomieju do you have opinions on this?
op_crates/crypto/01_crypto.js
Outdated
const input = data.slice(0); | ||
const output = new ArrayBuffer(digestByteLengths[alg.name]); | ||
await core.jsonOpAsync( | ||
"op_subtle_digest", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if all ops in the crypto op crate would start with with op_crypto
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall I also rename the one for getRandomValues()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes please.
Yeah I agree; unstable is superfluous in this case. |
I'll split the PR the following way:
I'll keep reporting on the status of the overall task on this PR as #1891 is a bit too cluttered for that. Else, I can create a new issue to replace #1891. It's however you want. |
d219dc5
to
7405496
Compare
21c610a
to
9efdb00
Compare
The PR is up-to-date with The type definitions have been moved to the op crate, following the convention set by the others (web, fetch and websocket). |
@lucacasonato @bartlomieju I think it is ready for merging. |
Could you enable the relevant wpt tests? see the contributing docs for how the new wpt runner works. |
This PR has no implementation. The tests shouldn't be enabled. They will be selectively added as more crypto functions are implemented. |
@yacinehmito thanks for the PR, there's a lot of changes but I can find any implementation of actual APIs. Is this a rebase mistake? If not then I'd prefer to land a PR that contains implementation of at least one algo. |
// a provided algorithm. We store it here to prevent prototype pollution. | ||
const toUpperCase = String.prototype.toUpperCase; | ||
|
||
class RegisteredAlgorithmsContainer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is never used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is meant to be used in further PRs.
op_crates/crypto/Cargo.toml
Outdated
ring = "0.16.19" | ||
tokio = { version = "1.1.1", features = ["full"] } | ||
serde_json = { version = "1.0.61", features = ["preserve_order"] } | ||
serde = { version = "1.0.121", features = ["derive"] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are all those dependencies added here? serde
and serde_json
are reexported from deno_core
anyway, so no need to declare them here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bartlomieju All other op_crates include serde
as a dependency. Why would this one be any different?
If they shouldn't, I'd be happy to open a PR to remove them.
@bartlomieju It used to have one, but if you go back the comments thread you'll see that I was asked to separate implementations in multiple PRs. |
Okay I can see what you're going for here. I am strongly against implementing throwing stubs; there are packages in the wild that do progressive enhancement so these should just still be undefined so they can be tested for using normal property lookups. |
This is sensible, and why I suggested to put this behind In the meantime I will work on other implementations and open draft PRs as I go. Let me know once you have a definitive plan that people are aligned on, and I will execute on it. |
@caspervonb @bartlomieju @lucacasonato Exploring optionsIntroduce the ops one by one, then the TypeScript declaration lastIt's very simple to do, and nobody using TypeScript will see the WebCrypto API until it is fully implemented. However, the API would still be exposed because we need it for wpt tests. Introduce the API first, then the ops one by oneThis is what this PR does. Undesirable at this leads to runtime errors. We are bettern Put the API behind the unstable flagWould allow us plenty of iteration without being too concerned about exposing a half-finished API. Proposition already rejected though. Use a feature branchHave a branch dedicated to implementing the WebCrypto API, then merge smaller PRs into it. Would allow for releasing the WebCrypto API at once, but may lead to integration issues if the branch is not constantly rebased. Thoughts? |
d931930
to
3c8a26d
Compare
3c8a26d
to
9bbde5a
Compare
This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions. |
Is there any chance this PR could be recovered? Are there any other efforts/plans to add Web Crypto? |
Yeah, it's coming soon. |
First step to solve #1891.
This PR adds the common code for the Web Crypto API, i.e.:
Progress
We document progress on solving #1891 here:
keyUsages
.worker.js
test suites (test(wpt): Enable wpt workers tests #9065)decrypt()
deriveBits()
deriveKey()
digest()
(feat(op_crates/crypto): Implement crypto.subtle.digest() #9069)encrypt()
exportKey()
generateKey()
importKey()
sign()
unwrapKey()
verify()
wrapKey()
getRandomValues()
spec-compliant (fix(op_crates/crypto): Test crypto.getRandomValues() with wpt #9016)op_get_random_values
toop_crypto_get_random_values
(fix(op_crates/crypto): Prefix op of getRandomValues() by op_crypto #9067)Design
Type definitions
The type definitions have been taken from https://github.com/microsoft/TypeScript/blob/2428ade1a91248e847f3e1561e31a9426650efee/src/lib/webworker.generated.d.ts#L3060.
They are consistent with the W3C recommendation.
I made a slight change regarding the
keyUsages
parameters by giving them the typeIterable<KeyUsage>
instead ofKeyUsage[]
. Indeed, Chrome, Firefox and Safari all accept iterables of strings, not just arrays (the specs don't specify the exact type; they just mention "sequences").JavaScript wrapper
crypto.subtle
I went with a simple literal object for
crypto.subtle
, as we had withcrypto
itself. It's not how Chrome, Firefox and Safari do it: they expose the "classes"Crypto
andSubtleCrypto
, which can't be instantiated. The methods ofcrypto
andcrypto.subtle
aren't in the object itself but on the respective prototype.Let me know if you want me to mirror that behaviour in Deno.
JS side vs. Rust side
The validation logic and the dictionary of algorithms are handled on the JS side because:
Returning buffers
We instantiate buffers on the JS side as
ArrayBuffer
, pass them wrapped in anUint8Array
to the Rust side as zero-copy buffers, then fill them with the data that is supposed to be returned.That way, we don't have to serialise the data returned by the underlying crypto library when it is large.
However, this assumes that we know in advance the size of the
ArrayBuffer
on the JS side. So far, this hasn't been a big issue, but it does add complexity.It would be handy if we could instantiate an
ArrayBuffer
directly on the Rust side and return a handle to the JS side.Underlying crypto library
Deliberating which crypto library to pick
It's not clear which crypto library I should use to implement the Rust ops.
I require some guidance as to which library should be included. Once it's decided, I expect the implementations to be rather easy.
We will go for ring, as per @lucacasonato's recommendation. It won't be enough to implement all operations, but we'll see when that comes up.
Testing
Chrome, Firefox and Safari all share the same test suite for the Web Crypto API, as they just vendored the relevant tests from web-platform-tests. I figured that the safest and easiest way to test the Web Crypto API on deno would be to do the same.
However, web-platform-tests are meant to run in browsers. There is a heavy reliance on a DOM, and scripts are assumed to be running in the global scope. To be able to run those tests unchanged in Deno, we would need to reimplement their JavaScript test harness from scratch.
I deemed this not worth the effort, and settled on rewriting the tests so that they can directly run as unit tests in Deno. It has the advantage of making the test suite more idiomatic (and arguably easier to read as it's in modern TypeScript, as opposed to ES5). However it makes it all the more challenging to follow upstream. I don't think it's a big deal given the very slow rate of changes there, but it ought to be mentioned.@lucacasonato added the the web-platform-tests as part of #8990.