-
Notifications
You must be signed in to change notification settings - Fork 570
WIP: WebcryptoAPI support #558
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
base: master
Are you sure you want to change the base?
Conversation
build/build.go
Outdated
@@ -290,7 +290,7 @@ func parseAndAugment(pkg *build.Package, isTest bool, fileSet *token.FileSet) ([ | |||
switch d := decl.(type) { | |||
case *ast.FuncDecl: | |||
if replacedDeclNames[funcName(d)] { | |||
d.Name = ast.NewIdent("_") | |||
d.Name.Name = "_gopherjs_overriden_" + d.Name.String() |
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 you wrote here can be simplified slightly to
d.Name.Name = "_gopherjs_overriden_" + d.Name.Name
.If you're assigning to
d.Name.Name
, you must knowd.Name
isn'tnil
, so callingd.Name.String()
instead of justd.Name.Name
isn't needed. -
The word "overriden" is misspelled, it should be "overridden".
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.
You've addressed point 2 but still not 1. Did you miss it, or are you not making the change because you disagree it's an improvement?
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.
Sorry, that was a mistake from me. I'll update the code.
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.
Point 1 is now fixed as suggested.
Nice, that's promising! I will try it out on a larger project (that imports more of standard library) and see how it does. It makes sense that the difference wouldn't be huge, because GopherJS overrides a modest, not large, amount of standard library identifiers. So even if they're now included, it's likely not to be a big deal. And if nothing references them, it's possible DCE still eliminates them. I don't know how advanced the current DCE algorithm is and how this will affect it, perhaps @neelance would know. |
"github.com/gopherjs/gopherjs/js" | ||
) | ||
|
||
var ErrWebCryptoFailed = errors.New("crypto: Web Crypto API call failed") |
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.
Should this be an exported variable? I don't think we want user code to be able to use it.
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 right with the current code, but in the near future this error code will be useful for each of the modules under 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.
Can you elaborate on how it will be used?
I'm concerned about this because I don't think we should be adding to the API surface of crypto/...
packages in standard library, as that would make user code incompatible with Go.
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 you elaborate on how it will be used?
The idea is to have a specific error code returned when a webcrypto method fails with a NotSupportedError
. Then most cryptographic functions implementation (in each crypto/*
package) would look like:
func foo() error {
err := webcrypto_foo()
if err == ErrWebCryptoUnsupported {
err = _gopherjs_overriden_foo()
}
return err
}
In order to make this error code visible from every package under crypto
, the only way I can think of is to make it public.
that would make user code incompatible with Go.
I didn't think about that. This code is not meant to be used by calling applications, but only in gopherjs, in the various crypto
packages.
In the last version of the code, I moved the non standard public stuff in a new package crypto/webcrypto. Is that ok? Should we put this kind of code somewhere else (like under the js
package) ?
} | ||
} | ||
return subtle | ||
} |
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.
Would it make more sense to execute this code once, at package init time, and save results?
Instead of calling it once for every crypto.Sum256
call.
Similar to how net/http
package does it for figuring out the transport to use.
case resSlice:= <-resCh: | ||
copy(res[:], resSlice) | ||
} | ||
return |
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.
Style-wise, I highly suggest avoiding naked returns because they're less readable.
Naked returns are okay if the function is a handful of lines. Once it's a medium-sized function, be explicit with your return values. Corollary: it's not worth it to name result parameters just because it enables you to use naked returns.
Source: https://github.com/golang/go/wiki/CodeReviewComments#naked-returns
Overall, this looks really good! Thanks for working on this @r-l-x. I've left some comments that came to mind looking at it, but this is a great start. |
// The WebCryptoAPI call failed: fallback by calling the implementation from | ||
// the Go standard library | ||
return _gopherjs_overriden_Sum256(data) | ||
} |
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.
Under what circumstances might an error occur here? Clearly, if the subtle API isn't supported, but if the subtle API itself returns an error, we'll also fall back to the Go standard library.
I wonder if there might ever be a situation where subtle is present, but constantly failing, which would lead to worse performance (by virtue of calling JS then Go every time), rather than better. Maybe that will never happen, so my concern might be unwarranted.
But if the concern has any validity, perhaps it would make sense to de-register the subtle API if an error is returned, so that subsequent calls go straight to the Go standard lib?
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.
That's a good point.
Generally speaking for crypto functions, the only cases I can think of where I would expect this to happen are:
- When the function or one of the parameters is not supported (but the function is still defined). I'm not sure which exception we will get though, I guess a NotSupportedError.
- With Chrome when the origin is insecure (NotSupportedError).
- When there is a problem with the arguments, e.g. (depending on the algorithm) decryption with a wrong key or wrong authentication tag (According to the W3C spec, we should get an OperationError in this case).
We could try to check if the error code is NotSupportedError and then remember that the webcrypto implementation of this function shouldn't be called, but NotSupportedError may also mean that while the function itself is supported, a parameter, typically a key length or mode of operation is not supported (see https://diafygi.github.io/webcrypto-examples/), so that would be counterproductive in this case to globally avoid the JS implementation of the function.
The performance impact of calling the JS implementation when we get a NotSupportedError is about 5ms under Chrome on insecure origin: http://www.gopherjs.org/playground/#/jlVJjSdJeh
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.
5ms doesn't sound like enough to worry about to me. At least not at this stage. Micro-optimizations like that should come much later :)
I guess the next step is to implement a generic way to call a JS function returning a promise, so that we don't have to create a channel, implement the calllbacks and call @shurcooL do you think we could add a |
Another point that would be really useful is to be able to check the error type when a promise fails. Typically, for a decryption function, depending on the mode (let's say AES-GCM), an So I would typically like to call What would you recommend in order to do this in gopherjs ? |
If you don't want to depend on |
This can also be done with No reason you can't abstract that into your own helper function(s) for use in this lib, though. I did something similar to wrap promises in my go-pouchdb package. Maybe the same approach would work here? |
Yes, I agree. I was thinking that it would be convenient to have a method with a |
That does indeed sound generally useful. Something like:
? Might be worth creating a separate issue for this. I would probably use that if it existed. Although it might be better handled in a separate library. |
Another thing that would be really helpful: Is there an easy way to launch the tests of the Go standard library in the browser ? |
Not sure if it helps, but this was the best way I could find to run tests in the browser. Very useful when I was working on the crypto stuff with different node/browser behaviour. It may be possible to use by importing that package into the stdlib tests, but I've not tried it. |
return nil, ErrCryptoNotFound | ||
} | ||
|
||
// First check that the method exists (if it doesn't, Call() would panic, so it's better to check first) |
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.
Not saying this is a bad approach, but an alternative would be to catch the panic with a recover
in a deferred function. Doing so might also be a bit more robust, as it would catch any other runtime panics in an odd JS implementation or something.
You can compile any test to a .js file, then load it in a browser and watch the JS console for output. It's a bit cumbersome, but it works. I spent some time about a year ago on trying to make this work better, but the GopherJS runtime is missing some features that I think are essential to automate the process. |
I haven't tried and don't know if it's possible, but can you change the kind of a top-level package identifier? E.g., the real func Sum256(data []byte) [Size]byte {
...
} If we can change the kind while overriding, we could make it a variable, like this: var Sum256 = func() func(data []byte) [Size]byte {
if webCryptoAvailable {
return webCryptoImplementationOfSum256
}
return _gopherjs_overridden_Sum256
}() Have you already considered doing it like that? Thoughts? |
I see your point, but one of the problems with this kind of approach is to know when we should call the webcrypto implementation. It doesn't seem feasible to me to determine this a priori. Let's take a few examples:
So it makes sense to me to first call the webcrypto implementation and then, if it failed because the requested operation was not supported, call the go implementation. |
I'm looking at how to run the standard library tests in the browser. With gopherjs test, it's really easy to run in the command line:
But when I generate the code using
If I get it right, the trick is to inject the desired arguments in |
@r-l-x Yep, looks like you got it working, in case it is helpful, here's the original discussion with @shurcooL about tests: https://gophers.slack.com/archives/gopherjs/p1472378655000172 |
Ok the current implementation seems good to me, with great performance improvements. We could of course add more cryptographic fonction implementations later, but it would be actually quite tricky for most of them. What do you think ? |
// | 2.7kB | 2.400ms | 2.022ms* | 710ns | 160ns* | | ||
// | 100kB | 774ms | 24ms* | 215ms | 16ms* | | ||
// | 10MB | 1.632s | 41ms* | 1.76s | 74ms* | | ||
// +--------+-----------+-----------+-----------+-----------+ |
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.
Do you have an easy way to run these benchmarks? I'd be curious to see how these results compare on different hardware configurations.
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 is the webcrypto version slower for small buffers? Where does it spend the time?
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.
@flimzy I mostly used the gopherJS playground, I don't have the link to the script right now, but I can add it to the discussion when I'm back on my PC.
@neelance I don't know where the time is spent, but from the measurements I did, there is definitely something specific to Chrome, it seems like a webcrypto promise cannot give an answer in less than 2ms. It would be interesting to see if it's the same for any promise. Firefox doesn't have this problem. I can try to profile it.
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.
@flimzy Here is a rather clean way to launch the benchmarks: http://www.gopherjs.org/playground/#/dPF9AaIxVg. Works as is with Firefox, but with Chrome in order to avoid the insecure origin problem, you have to copy the code, load the playground over HTTPS while accepting the security warning and paste the code.
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.
@neelance From the profiling I did (using this in Chrome: http://www.gopherjs.org/playground/#/dPF9AaIxVg http://www.gopherjs.org/playground/#/7uJwxm1UYd), the main thread is idle for most of the time, waiting for the response from the webcrypto API. I'm not sure how to profile the webcryptoAPI thread.
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.
@neelance I think you are right about the channel operation. This example seems to show that the latency occurs between the channel write (in the promise callback) and the channel read outside the promise: http://www.gopherjs.org/playground/#/btm0GV_mQY (2ms per operation).
On the opposite, this example http://www.gopherjs.org/playground/#/uhe5jIrcpA shows that the function call and the hash computation is pretty fast (70µs in my test).
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.
Hey @r-l-x, could you please try the benchmarks again with the scheduler
branch of GopherJS? I think it may resolve the 2ms latencies you were seeing.
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.
The branch got merged into master
, please test that one instead. :-)
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.
@neelance is latest master deployed to gopherjs playground?
Running https://gopherjs.github.io/playground/#/xN-qiC0TQk on gopherjs playground right now results in len 2000 being slightly faster for webcrypto, 1750 being about equal, and <=1500 being faster for the go implementation
(running on ubuntu16.04 x64 with chrome)
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.
Also ran the 'stop benchmark after result, before sending result in channel' test and am seeing webcrypto faster until 64 bytes and less
(Merged the results from the above test and the one from the previous comment)
len: 1
go 20000 58100 ns/op 0.02 MB/s
webcrypto 1000 1267000 ns/op 0.00 MB/s
webcrypto No Chan 133800 ns/op
len: 64
go 10000 103200 ns/op 0.62 MB/s
webcrypto 1000 1255000 ns/op 0.05 MB/s
webcrypto No Chan 134000 ns/op
len: 128
go 10000 142300 ns/op 0.90 MB/s
webcrypto 1000 1248000 ns/op 0.10 MB/s
webcrypto No Chan 134800 ns/op
len: 256
go 5000 221800 ns/op 1.15 MB/s
webcrypto 1000 1258000 ns/op 0.20 MB/s
webcrypto No Chan 137200 ns/op
len: 512
go 3000 402000 ns/op 1.27 MB/s
webcrypto 1000 1255000 ns/op 0.41 MB/s
len: 1024
go 2000 705000 ns/op 1.45 MB/s
webcrypto 1000 1256000 ns/op 0.82 MB/s
len: 1200
go 2000 824000 ns/op 1.46 MB/s
webcrypto 1000 1246000 ns/op 0.96 MB/s
len: 1500
go 2000 1012500 ns/op 1.48 MB/s
webcrypto 1000 1254000 ns/op 1.20 MB/s
I agree, let's leave out additional enhancements for future PRs. It's better to get this one fully reviewed and merged first. |
@@ -5,6 +5,7 @@ package rand | |||
import ( | |||
"errors" | |||
|
|||
"github.com/gopherjs/gopherjs/compiler/natives/src/crypto/webcrypto" |
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 think github.com/gopherjs/gopherjs/js/webcrypto
might be better, since natives
is not meant to be imported directly.
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 did the move: 02e0350
if crypto != js.Undefined { | ||
if crypto.Get("getRandomValues") != js.Undefined { | ||
if webcrypto.Crypto != nil { | ||
if webcrypto.Crypto.Get("getRandomValues") != js.Undefined { |
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 merge the if
statements.
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.
done.
Yes, I'm not sure why, but in order to see the error with CircleCI, I need to fold and unfold the red part. Here is our error:
I guess running the same test without the minify option should give a more exploitable log. I should have a bit of time today to try that. |
Here is the error without minify:
|
The only reference to a |
Thanks for the tip, that works for me.
It is indeed disabled from running that test, as you said. However, due to this change in this PR: // Allow overridden function calls
// The standard library implementation of foo() becomes _gopherjs_overridden_foo()
if replacedDeclNames[funcName(d)] {
d.Name = ast.NewIdent("_")
d.Name.Name = "_gopherjs_overridden_" + d.Name.Name
} It's no longer disabled from being compiled. I suspect there may be a bug with deadcode elimination not handling this case well. /cc @neelance However, I'm not able to reproduce it by doing something like this. Perhaps we should factor out that change and investigate it separately. We should also confirm that it doesn't have a significant negative effect on generated file sizes. |
I've confirmed locally that's the case: gopherjs $ gopherjs test --short reflect
PASS
ok reflect 3.742s
gopherjs $ open build/build.go
gopherjs $ git diff
diff --git a/build/build.go b/build/build.go
index 47a889e..260d242 100644
--- a/build/build.go
+++ b/build/build.go
@@ -308,7 +308,7 @@ func parseAndAugment(pkg *build.Package, isTest bool, fileSet *token.FileSet) ([
switch d := decl.(type) {
case *ast.FuncDecl:
if replacedDeclNames[funcName(d)] {
- d.Name = ast.NewIdent("_")
+ d.Name.Name = "_gopherjs_overridden_" + d.Name.Name
}
case *ast.GenDecl:
switch d.Tok {
gopherjs $ go install -v
github.com/gopherjs/gopherjs/build
github.com/gopherjs/gopherjs
gopherjs $ gopherjs test --short reflect
/Users/Dmitri/Dropbox/Work/2013/GoLand/src/github.com/gopherjs/gopherjs/test.730446537:37375
})();
^
ReferenceError: T1inner is not defined
at /Users/Dmitri/Dropbox/Work/2013/GoLand/src/github.com/gopherjs/gopherjs/test.730446537:36664:78
at Object.<anonymous> (/Users/Dmitri/Dropbox/Work/2013/GoLand/src/github.com/gopherjs/gopherjs/test.730446537:37375:3)
at Object.<anonymous> (/Users/Dmitri/Dropbox/Work/2013/GoLand/src/github.com/gopherjs/gopherjs/test.730446537:43739:4)
at Module._compile (module.js:571:32)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:488:32)
at tryModuleLoad (module.js:447:12)
at Function.Module._load (module.js:439:3)
at Module.runMain (module.js:605:10)
at run (bootstrap_node.js:422:7)
at startup (bootstrap_node.js:143:9)
at bootstrap_node.js:537:3
FAIL reflect 0.659s
gopherjs $ |
Looking into it further, I think it may be a DCE/miscompilation issue introduced by that change itself. If so, that means it'll be more complicated to support overriding existing funcs than the 1 line change above. |
c184f71
to
a484d0e
Compare
Hello again. FYI i'm currently working to fix this by specifying which of the overridden functions should be kept, using a syntax in the function comments, like:
This is similar to the go generate directives. |
About A lot of code GopherJS already deals with preventing DCE by assigning to Of course, if it's really unavoidable to make things work here, then it's ok. |
Before this commit, every overriden standard library function was renamed '_'. Now, if there is the following directive in the function comment: "//gopherjs:keep_overridden", they are prefixed with "_gopherjs_overridden_" so that they can be called from the overriding function. This is typically useful to use the standard library implementation as a fallback.
The overriding function tries to call the corresponding WebCryptoAPI function (which is much faster) when available, and fallbacks using the standard library implementation.
I don't see a way to implement crypto/ecdsa.Verify() based on webcrypto because the webcrypto implementation does include the hash where the go standard library interface doesn't. But this commit enables the webcrypto implementation for the cerificate signature checks made from the X.509 package.
This useful under Safari, when calling an unimplemented feature.
Hi, I just would like to have a feedback on this PR. |
@r-l-x Are you still interested in bringing this PR up to date for inclusion? #798 looks like it's close to being ready, which I understand would make this easier. @nevkontakte were discussiong, and we think it might be wise to guard this functionality behind a build tag, at least initially, as well. We might want to discuss including that in this PR. But before we get too far down that road, we'd like to know if you're interested in pursuing this further. Thanks. |
Warning: This branch is based on the branch from #798
This PR implements a few cryptographic functions using HTML5 Web Crypto API when available. This allows a major performance boost when compared to the basic go standard library implementation.
The HTML5 Web Crypto API is available in most in recent browsers, but please note that with Chrome, the page must be loaded from a secure origin (over HTTPS or from localhost). If there is a problem when calling the Web Crypto API, the code uses the Go standard library implementation as a fallback.
This PR optimizes the following functions:
sha256.Sum256
Hashing a 10MB buffer now takes 45ms with Chrome instead of 1400ms (30x faster)
x509.checkSignature
(for ECDSA and RSA signatures)This optimizes ECDSA and RSA signatures signature checks during X509 certificates verification (typically
Certificate.Verify
).x509.TestECDSA
now runs in 60ms instead of 20s (300x faster). Note that DSA signatures are not supported by the Web Crypto API.Note that the same approach cannot be used to optimize asymmetric signing or signature check functions calls (e.g.
rsa.[Sign|Verify]
) as defined in the Go standard library, because in the standard library the digest is computed outside the function, but when using the WebCryptoAPI functions, the digest must be computed by the function itself.See #554