Skip to content

Fix race condition within A4A verify#6837

Merged
lannka merged 18 commits intoampproject:masterfrom
google:a4a_verify_race_fix
Feb 7, 2017
Merged

Fix race condition within A4A verify#6837
lannka merged 18 commits intoampproject:masterfrom
google:a4a_verify_race_fix

Conversation

@keithwrightbos
Copy link
Copy Markdown
Contributor

@keithwrightbos keithwrightbos commented Dec 29, 2016

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.

// service, keyInfoPromise, can verify the signature, then the
// creative is valid AMP.
return some(keyInfoSet.map(keyInfoPromise => {
return !alreadyVerified() && some(keyInfoSet.map(keyInfoPromise => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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?

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.

Modified so that checkAlreadyVerified function returns Promise.reject or Promise.resolve after which I call then... let me know what you think.

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'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 => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

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.

// Reject to ensure the some operation waits for other
// possible providers to properly verify and resolve.
return Promise.reject(
`${keyInfo.serviceName} failed to verify`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

let verificationFound = false;
const alreadyVerified = () => {
return verificationFound ?
Promise.reject('verification already found') : null;
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 are you returning a promise instance if we just ! the value?

// service, keyInfoPromise, can verify the signature, then the
// creative is valid AMP.
return some(keyInfoSet.map(keyInfoPromise => {
return !alreadyVerified() && some(keyInfoSet.map(keyInfoPromise => {
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'd drop this alreadyVerified function entirely for an if:

if (verificationFound) {
  return Promise.reject('noop');
}
return some(...);

// 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;
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.

verified?

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.

Done

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

@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!

@lannka
Copy link
Copy Markdown
Contributor

lannka commented Jan 11, 2017

@keithwrightbos promoting the CID code to AMP core has been in our backlog for a while: #4898 .
One thing blocked the work is we wanted to lazy load the sha384 polyfill, otherwise it can increase runtime code size by about 10k (by my original rough estimation). Actually, can you check the compiled v0.js, see what the real value is, so we can decide if it's worth for a lazy load.

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

@lannka v0.js at current head is 188k vs 200k in PR (+6%) Let me know how you want to proceed

@lannka
Copy link
Copy Markdown
Contributor

lannka commented Jan 11, 2017

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?

Copy link
Copy Markdown

@tdrl tdrl left a comment

Choose a reason for hiding this comment

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

Still reading, but wanted to send comments so far.

verifyHashVersion,
PublicKeyInfoDef,
} from './crypto-verifier';
import {PublicKeyInfoDef, cryptoFor} from '../../../src/crypto';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 = ...

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.

Done

// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pedantic uber-nit: "... found as ..." => "... found, as ..."

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.

Done

.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`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: Feels like the message needs another word. "All keys for...", "All keys from..."...?

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.

Done

}, () => {
// rejection occurs if all providers fail to verify.
this.protectedEmitLifecycleEvent_('adResponseValidateEnd');
return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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.

Done

}))
// some() returns an array of which we only need a single value.
.then(returnedArray => returnedArray[0]);
.then(returnedArray => returnedArray[0], () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

Will leave as is unless AMP owners object

Copy link
Copy Markdown

@tdrl tdrl left a comment

Choose a reason for hiding this comment

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

LGTM, though possibly @avimehta should double-check the parts that touch amp-analytics crypto?

});

it('properly handles multiple providers that all fail', () => {
// Single provider with first key fails but second key passes validation
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not sure I understand this comment. Test name claims that all providers fail, but if one key passes wouldn't some provider pass?

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.

Done

// 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  1. true verifier resolves before false verifier.
  2. false resolves before true.
  3. One verify rejects before the other returns true.
  4. One verify rejects before the other returns false.
  5. true resolves before a reject.
  6. false resolves before a reject.

I think that's all the cases?

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.

@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')));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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?

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.

This has changed after @lannka PR to make lazily inflate of polyfill

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

@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)

@lannka
Copy link
Copy Markdown
Contributor

lannka commented Feb 3, 2017

@keithwrightbos could you resolve the conflicts?

@jridgewell jridgewell dismissed their stale review February 3, 2017 19:36

Don't remember the review. 😬

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

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"]}.

@lannka
Copy link
Copy Markdown
Contributor

lannka commented Feb 3, 2017

did you forget to push? We do see a list of conflicting files here.

src/service/crypto-impl.js must not depend on third_party/closure-library/sha384-generated.js.

After you merging with the latest code, src/service/crypto-impl.js should not be depending on the lib directly.

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

I accidentally pushed to upstream instead of origin. Should be good now. Apologies.

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

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' ]
Error compiling ./3p/integration.js
WARNING - Failed to load module "./crypto-impl.js"

Copy link
Copy Markdown
Contributor

@lannka lannka left a comment

Choose a reason for hiding this comment

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

Some minor comments

new Uint8Array(creative),
signature,
keyInfo)
.then(isValid => {
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.

the indentation is a bit off, is there enough space to fit all params in one line?

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.

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');
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: just keep reason empty?

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.

Done

if (keyInfo && verifyHashVersion(signature, keyInfo)) {
if (keyInfo &&
this.crypto_.verifyHashVersion(signature, keyInfo)) {
user().error(TAG, this.element.getAttribute('type'),
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.

should be a dev error instead? and how likely this is to occur?

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 should be very infrequent and will result in loss of preferential treatment. Does dev error send error so we can track frequency?

`${keyInfo.serviceName} key failed to verify`);
},
err => {
user().error(
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.

same 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.

Done

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

@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' ]
Error compiling ./3p/integration.js
WARNING - Failed to load module "./crypto-impl.js"

@keithwrightbos
Copy link
Copy Markdown
Contributor Author

@lannka PTAL. Note that I had to revert to Promise.reject() otherwise presubmit fails indicating reject must have reason. Please merge when green. Thanks!

@lannka lannka merged commit 211119b into ampproject:master Feb 7, 2017
@taymonbeal taymonbeal deleted the a4a_verify_race_fix branch February 7, 2017 16:23
torch2424 pushed a commit to torch2424/amphtml that referenced this pull request Feb 14, 2017
* 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
mrjoro pushed a commit to mrjoro/amphtml that referenced this pull request Apr 28, 2017
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants