Fix race condition within A4A verify#6837
Fix race condition within A4A verify#6837lannka merged 18 commits intoampproject:masterfrom google:a4a_verify_race_fix
Conversation
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| // service, keyInfoPromise, can verify the signature, then the | ||
| // creative is valid AMP. | ||
| return some(keyInfoSet.map(keyInfoPromise => { | ||
| return !alreadyVerified() && some(keyInfoSet.map(keyInfoPromise => { |
There was a problem hiding this comment.
Won't this drop the Promise.reject() on the floor when verificationFound? !Promise.reject('foo') == false, so you'll go on to the some clause, but the Promise will be uncaptured and produce an uncaught promise exception, won't it?
There was a problem hiding this comment.
Modified so that checkAlreadyVerified function returns Promise.reject or Promise.resolve after which I call then... let me know what you think.
There was a problem hiding this comment.
I'd drop this alreadyVerified function entirely for an if:
if (verificationFound) {
return Promise.reject('noop');
}
return some(...);| }; | ||
| return some(keyInfoSetPromises.map(keyInfoSetPromise => { | ||
| // Resolve Promise into Array of Promises of signing keys. | ||
| return keyInfoSetPromise.then(keyInfoSet => { |
There was a problem hiding this comment.
Would it be worth having an alreadyVerified check here as well? I still am not sure I'm tracking the logic of this method fully, but I think you can be in the case where a previous keyInfoSetPromise has already verified the sig. Is that right? If so, you could skip out entirely here.
There was a problem hiding this comment.
Array.prototype.map is not an async operation but instead executes the given function on each item in the array and returns an array whose value is the result of the given function in order of the items in the array. Given its not async, I see no need to recheck already verified.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| // Reject to ensure the some operation waits for other | ||
| // possible providers to properly verify and resolve. | ||
| return Promise.reject( | ||
| `${keyInfo.serviceName} failed to verify`); |
There was a problem hiding this comment.
I know that writing unit tests for async code / testing race conditions is a nightmare, but I'd be a lot more comfortable if we could have one or more tests that fails when the race condition is triggered, but passes with your change to the code here.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| let verificationFound = false; | ||
| const alreadyVerified = () => { | ||
| return verificationFound ? | ||
| Promise.reject('verification already found') : null; |
There was a problem hiding this comment.
Why are you returning a promise instance if we just ! the value?
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| // service, keyInfoPromise, can verify the signature, then the | ||
| // creative is valid AMP. | ||
| return some(keyInfoSet.map(keyInfoPromise => { | ||
| return !alreadyVerified() && some(keyInfoSet.map(keyInfoPromise => { |
There was a problem hiding this comment.
I'd drop this alreadyVerified function entirely for an if:
if (verificationFound) {
return Promise.reject('noop');
}
return some(...);
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| // Track if verification found as it will ensure that promises yet to | ||
| // resolve will "cancel" as soon as possible saving unnecessary resource | ||
| // allocation. | ||
| let verificationFound = false; |
|
@jridgewell & @tdrl PTAL. Note that in order to easy in creating test coverage, I moved crypto into a proper service which resulted in combining crypto usage across amp-analytics and a4a. Ideally I could get this into today's canary cut. Thanks! |
|
@keithwrightbos promoting the CID code to AMP core has been in our backlog for a while: #4898 . |
|
@lannka v0.js at current head is 188k vs 200k in PR (+6%) Let me know how you want to proceed |
|
6% is non-neglectable. At this point, are you able to split that part out of this PR, and get the bug fix in first? |
tdrl
left a comment
There was a problem hiding this comment.
Still reading, but wanted to send comments so far.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| verifyHashVersion, | ||
| PublicKeyInfoDef, | ||
| } from './crypto-verifier'; | ||
| import {PublicKeyInfoDef, cryptoFor} from '../../../src/crypto'; |
There was a problem hiding this comment.
Do you actually need to import PublicKeyInfoDef if you're just using it for typing? I thought you could get away without it. IIRC, you then declare your params like:
/** @type {!../../../src/crypto.PublicKeyInfoDef} */
let foo = ...
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| // signature, then the creative is valid AMP. | ||
| /** @type {!AllServicesCryptoKeysDef} */ | ||
| const keyInfoSetPromises = this.win.ampA4aValidationKeys; | ||
| // Track if verification found as it will ensure that promises yet to |
There was a problem hiding this comment.
Pedantic uber-nit: "... found as ..." => "... found, as ..."
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| .then(returnedArray => returnedArray[0], () => { | ||
| // Rejection occurs if all keys for this provider fail to validate. | ||
| return Promise.reject( | ||
| `All keys ${keyInfoSet.serviceName} failed to verify`); |
There was a problem hiding this comment.
Nit: Feels like the message needs another word. "All keys for...", "All keys from..."...?
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| }, () => { | ||
| // rejection occurs if all providers fail to verify. | ||
| this.protectedEmitLifecycleEvent_('adResponseValidateEnd'); | ||
| return null; |
There was a problem hiding this comment.
I think this will cause the whole chain to be fulfilled, not rejected. If I read MDN properly, returning anything other than a reject from a .then( , onRejected) block, then the promise is considered resolved. Is that the semantic you want here? I'd think you'd want something like return Promise.reject('No validation service could verify this key')?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
| })) | ||
| // some() returns an array of which we only need a single value. | ||
| .then(returnedArray => returnedArray[0]); | ||
| .then(returnedArray => returnedArray[0], () => { |
There was a problem hiding this comment.
Style debate: Should we prefer .then(onFulfilled, onRejected) style or .then(onFullfilled).catch(onRejected) style? I feel the latter is slightly more readable, but I'm flexible. I haven't surveyed the AMP code base to see if there's a clear preference.
There was a problem hiding this comment.
Will leave as is unless AMP owners object
| }); | ||
|
|
||
| it('properly handles multiple providers that all fail', () => { | ||
| // Single provider with first key fails but second key passes validation |
There was a problem hiding this comment.
Not sure I understand this comment. Test name claims that all providers fail, but if one key passes wouldn't some provider pass?
| // From testing have found that need to yield prior to calling last | ||
| // key info resolver to ensure previous keys have had a chance to | ||
| // execute. | ||
| setTimeout(() => {keyInfoResolver({}); }, 0); |
There was a problem hiding this comment.
Spacing: I think you either need a space before keyInfo... or to delete one before the following }.
General: If I understand this right, this is delaying the third promise until after the first two have resolved and showing that the verifier will only call the first two, because one of them returns true, right?
So let's think... We want to test that the race condition is eliminated, which means that we need to make sure that it behaves correctly regardless of order of operations. So I think (?) that means that we need the following tests:
trueverifier resolves beforefalseverifier.falseresolves beforetrue.- One verify rejects before the other returns
true. - One verify rejects before the other returns
false. trueresolves before a reject.falseresolves before a reject.
I think that's all the cases?
There was a problem hiding this comment.
@taymonbeal is working on modifying key signature to ensure we know explicitly which provider and public key to use so this will be going away. As a result, I would prefer not to invest too much into additional test coverage given priorities to work on other items.
| !../extensions/amp-analytics/0.1/crypto-impl.Crypto>} */ ( | ||
| getElementService(window, 'crypto', 'amp-analytics'))); | ||
| return (/** @type {!./service/crypto-impl.Crypto} */ ( | ||
| getExistingServiceForWindow(window, 'crypto'))); |
There was a problem hiding this comment.
Right -- so I've been unclear why the old API returned a promise and the new one doesn't. I assume that you're now doing an eager initialization, while it was formerly doing a lazy one? Is that a problem? Like, are you incurring some significant initialization cost on pages that don't need crypto services or something?
There was a problem hiding this comment.
This has changed after @lannka PR to make lazily inflate of polyfill
|
@lannka PTAL - updated to take your lazy crypto polyfill change into account where amp-analytics crypto has been moved into new crypto-impl service along with A4A specific items. This will allow for code re-use across A4A implementations (AdSense, Doubleclick, Triplelift) |
|
@keithwrightbos could you resolve the conflicts? |
|
I just synced to upstream master and didn't get any conflicts that it could not resolve automatically. In addition, build seems to be showing the following error from gulp dep-check yet when I run locally I don't see any issues. Any ideas? Thanks src/service/crypto-impl.js must not depend on third_party/closure-library/sha384-generated.js. Rule: {"filesMatching":"/*.js","mustNotDependOn":"third_party//.js","whitelist":["extensions/amp-analytics/**/.js->third_party/closure-library/sha384-generated.js","extensions/amp-mustache/0.1/amp-mustache.js->third_party/mustache/mustache.js","3p/polyfills.js->third_party/babel/custom-babel-helpers.js","src/sanitizer.js->third_party/caja/html-sanitizer.js","extensions/amp-viz-vega/->third_party/vega/vega.js","extensions/amp-viz-vega/->third_party/d3/d3.js","src/dom.js->third_party/css-escape/css-escape.js","src/shadow-embed.js->third_party/webcomponentsjs/ShadowCSS.js"]}. |
|
did you forget to push? We do see a list of conflicting files here.
After you merging with the latest code, |
|
I accidentally pushed to upstream instead of origin. Should be good now. Apologies. |
|
Looks like gulp type-checks is failing and I'm not sure why. lannka@ any ideas? [16:53:46] Starting closure compiler for [ './3p/integration.js' ] |
| new Uint8Array(creative), | ||
| signature, | ||
| keyInfo) | ||
| .then(isValid => { |
There was a problem hiding this comment.
the indentation is a bit off, is there enough space to fit all params in one line?
There was a problem hiding this comment.
Cannot easily fit all on one line and linter is happy with as is
| // service, keyInfoPromise, can verify the signature, then the | ||
| // creative is valid AMP. | ||
| if (verified) { | ||
| return Promise.reject('noop'); |
There was a problem hiding this comment.
nit: just keep reason empty?
| if (keyInfo && verifyHashVersion(signature, keyInfo)) { | ||
| if (keyInfo && | ||
| this.crypto_.verifyHashVersion(signature, keyInfo)) { | ||
| user().error(TAG, this.element.getAttribute('type'), |
There was a problem hiding this comment.
should be a dev error instead? and how likely this is to occur?
There was a problem hiding this comment.
It should be very infrequent and will result in loss of preferential treatment. Does dev error send error so we can track frequency?
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| `${keyInfo.serviceName} key failed to verify`); | ||
| }, | ||
| err => { | ||
| user().error( |
|
@lannka PTAL. I'm still getting the following error on gulp check-types although gulp build does not error. Any ideas? [09:04:52] Starting closure compiler for [ './3p/integration.js' ] |
|
@lannka PTAL. Note that I had to revert to Promise.reject() otherwise presubmit fails indicating reject must have reason. Please merge when green. Thanks! |
* Address issue 6780 * Move crypto into its own service; add test coverage * fix merge conflicts * PR review * Move amp analytics into crypto service * PR review * PR reviews * test/presubmit fixes * fix linter error
* Address issue 6780 * Move crypto into its own service; add test coverage * fix merge conflicts * PR review * Move amp analytics into crypto service * PR review * PR reviews * test/presubmit fixes * fix linter error
See issue #6780 where some was returning on first provider failure to validate due to returning null instead of Promise.reject. Added optimization which guarantees any other awaiting promises "cancel" as soon as any provider successfully verifiers (reduces resource allocation).
Moves crypto into its owner service allowing for easier testing and ensuring that common code currently used by amp-analytics and a4a are utilized.