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

A4A protect emitLifecycleEvent/onAmpRender from exiting promise chain; log signing service to stackdrive with service name #6504

Merged
merged 9 commits into from Dec 14, 2016

Conversation

keithwrightbos
Copy link
Contributor

Couple of changes to A4A flow:

  1. Created generalized protectFunctionWrapper_ which ideally would use spread when calling optional on error handler but linter does not allow. Function will be used for any abstract function implementations whose failure is not expected to trigger abort (emitLifecycleEvent & onAmpCreativeRender)

  2. Modify crypto key fetch to associate service name with key promise allowing for later validation error logging to include service name

  3. Modify crypto validation to report to stackdriver via user().error() and to ensure error is only reported when failure is expected (when public key and signature start with same version info)

* @visibileForTesting
* @private
*/
export const protectFunctionWrapper_ = (fn, opt_this, opt_onError) => {
Copy link
Member

Choose a reason for hiding this comment

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

Would probably make more sense for this to be export function protectFunctionWrapper_(fn, opt_this, opt_onError) {.

Also, old-style optional parameters are no longer used; rename them and give explicit defaults (which can be undefined if you want the same semantics) instead.

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

@@ -482,8 +529,14 @@ export class AmpA4A extends AMP.BaseElement {
if (isValid) {
return creative;
}
return Promise.reject(
'Key failed to validate creative\'s signature.');
// Only report is signature is expected to match given
Copy link
Member

Choose a reason for hiding this comment

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

Typo: "if"

Copy link

Choose a reason for hiding this comment

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

Grammar nit: also, "... to match given ..." => "... to match, given that ..."

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

'Key failed to validate creative\'s signature.');
// Only report is signature is expected to match given
// multiple key providers could have been specified.
if (!verifyHashVersion(signature, keyInfo)) {
Copy link
Member

@taymonbeal taymonbeal Dec 6, 2016

Choose a reason for hiding this comment

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

Can you explain why this won't produce false positives? If you've got a locally built AMP runtime, this will run against both the dev and prod keys, and if the creative was signed with the prod key verifyHashVersion will return false for the dev key. Unless I've misunderstood something?

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, I had the inverse. Removed !.

if (!verifyHashVersion(signature, keyInfo)) {
user().error(TAG, this.element.getAttribute('type'),
`Key failed to validate creative\'s signature for
service ${keyInfo.serviceName}.`, keyInfo.cryptoKey);
Copy link
Member

Choose a reason for hiding this comment

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

That newline and leading whitespace won't be stripped, they'll appear in the error message.

Copy link
Member

Choose a reason for hiding this comment

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

Also, no need to escape a single quote in a non-single-quoted string.

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

@@ -90,19 +91,25 @@ describe('integration test: a4a', () => {
beforeEach(() => {
sandbox = sinon.sandbox.create();
xhrMock = sandbox.stub(Xhr.prototype, 'fetch');
// Expect key set fetches for signing services.
const fetchJsonMock = sandbox.stub(Xhr.prototype, 'fetchJson');
Copy link
Member

Choose a reason for hiding this comment

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

I really want a less fragile way to do test doubles for AJAX requests than these mocks. No need to fix that in this PR though, just something for the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack, I agree. I originally had fetch_ but it caused more extensive issues with the test than I wanted to address in this PR.

* onError handler (if none provided, error is swallowed).
* @param {!Function} fn to protect
* @param {T=} opt_this An optional object to use as the 'this' object
* when calling the function.
Copy link
Member

Choose a reason for hiding this comment

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

Should probably note explicitly that this defaults to undefined if omitted.

Copy link

Choose a reason for hiding this comment

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

Isn't that the usual semantic of omitted params in JS?

Copy link
Member

Choose a reason for hiding this comment

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

In terms of language semantics, yes. I was referring more to the API for this function; if a parameter is optional, it should be stated what happens if it's omitted (the answer in this case is that this is bound to the value undefined in the wrapped function, but I've seen other callback-accepting functions that do things differently).

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 (updated function documentation)

@@ -125,6 +126,38 @@ export const LIFECYCLE_STAGES = {
};


/**
* Utility function that ensures any error thrown is handled by optional
Copy link
Member

Choose a reason for hiding this comment

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

Can we be clear on whether the caller of a function wrapped with this can use its return value? If so, there should be explicit returns for the case where there's no opt_onError or opt_onError throws, and it should be documented what's returned in these cases. If not, the tests should be changed not to rely on this behavior.

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

@@ -125,6 +126,38 @@ export const LIFECYCLE_STAGES = {
};


/**
* Utility function that ensures any error thrown is handled by optional
* onError handler (if none provided, error is swallowed).
Copy link
Member

Choose a reason for hiding this comment

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

Make that "if none provided or handler throws"?

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

* and arguments provided to function call.
* @return {!Function} protected function
* @template T
* @visibileForTesting
Copy link
Member

Choose a reason for hiding this comment

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

Typo

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

expect(errorCalls).to.equal(0);
});

it('handles error properly', done => {
Copy link
Member

Choose a reason for hiding this comment

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

I'm having a hard time figuring out how/whether this test works as intended (largely because of the done callback, whose semantics in synchronous code I can't figure out from the Chai API documentation and which should probably be avoided when possible). If protectFunctionWrapper_'s API is such that callers can rely on its return value, then I think it would be better to return an explicit value from the handler and assert that the top-level expression equals that. If not, then I think you should do something analogous to the errorCalls variable in the test above this one.

Also, probably-soon-to-be-irrelevant style nit: parentheses are recommended around arrow function arguments.

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, agree that usage of done was unnecessary.

* @return {!Function} protected function
* @template T
* @visibileForTesting
* @private
Copy link

Choose a reason for hiding this comment

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

It looks weird to have it marked both @private and @visibleForTesting, but I guess there's never been a scope visibility thing enabled, so no problem. :-)

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 agree. Renamed to protectFunctionWrapper and removed @Private

@@ -482,8 +529,14 @@ export class AmpA4A extends AMP.BaseElement {
if (isValid) {
return creative;
}
return Promise.reject(
'Key failed to validate creative\'s signature.');
// Only report is signature is expected to match given
Copy link

Choose a reason for hiding this comment

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

Grammar nit: also, "... to match given ..." => "... to match, given that ..."

@@ -686,40 +739,46 @@ export class AmpA4A extends AMP.BaseElement {
const jwkSetPromises = this.getSigningServiceNames().map(serviceName => {
dev().assert(getMode().localDev || !endsWith(serviceName, '-dev'));
const url = signingServerURLs[serviceName];
const currServiceName = serviceName;
Copy link

Choose a reason for hiding this comment

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

Why rename serviceName here? Why not just make it currServiceName in the loop index?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah because then it could be re-assigned within the map callback prior to the xhr callback such that it references the LAST value in the object. It's possible map does not actually cause such an issue (would certainly be the case if this was a for loop) but I'm old school and want to ensure we're safe in case someone changes this to a loop at some point.

@@ -90,19 +91,25 @@ describe('integration test: a4a', () => {
beforeEach(() => {
sandbox = sinon.sandbox.create();
xhrMock = sandbox.stub(Xhr.prototype, 'fetch');
// Expect key set fetches for signing services.
const fetchJsonMock = sandbox.stub(Xhr.prototype, 'fetchJson');
for (const serviceName in signingServerURLs) {
Copy link

Choose a reason for hiding this comment

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

Last time I tried that, one of the AMP reviewers told me to avoid .keys(), in favor of for - in because .keys() involved allocating a list, which you want only for iteration.

@keithwrightbos
Copy link
Contributor Author

@taymonbeal PTAL

@@ -128,6 +129,42 @@ export const LIFECYCLE_STAGES = {
};


/**
* Utility function that ensures any error thrown is handled by optional
* onError handler (if none provided or handler throws, error is swallowed).
Copy link
Member

Choose a reason for hiding this comment

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

For an abundance of clarity, can this be "if none provided or handler throws, error is swallowed and undefined is returned"?

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

* @visibleForTesting
*/
export function protectFunctionWrapper(
fn, opt_this = undefined, opt_onError = undefined) {
Copy link
Member

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

/**
* Protected version of emitLifecycleEvent that ensures error does not
* cause promise chain to reject.
* @private {!function(string, !Object=)}
Copy link
Member

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

*/
this.protectedEmitLifecycleEvent_ = protectFunctionWrapper(
this.emitLifecycleEvent, this,
(err, var_args) => {
Copy link
Member

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

'Key failed to validate creative\'s signature.');
// Only report if signature is expected to match, given that
// multiple key providers could have been specified.
if (verifyHashVersion(signature, keyInfo)) {
Copy link
Member

Choose a reason for hiding this comment

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

What happens if the key hash in the signature doesn't match any available key? That would be an error case, but I don't see how it gets reported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added logging statement within promise chain when we had a signature but validation failed against all providers. This will cause multiple error statements to potentially occur (should there be one key provider that matches the signature hash but failed validation) but cannot see easy way around it.

@keithwrightbos
Copy link
Contributor Author

@taymonbeal PTAL

@keithwrightbos
Copy link
Contributor Author

@lannka @dvoytenko could you merge or assign another reviewer as needed? Thanks

@lannka lannka merged commit fcbe8f7 into ampproject:master Dec 14, 2016
@taymonbeal taymonbeal deleted the a4a_csi_protect branch December 16, 2016 20:15
Lith pushed a commit to Lith/amphtml that referenced this pull request Dec 22, 2016
…; log signing service to stackdrive with service name (ampproject#6504)

* Initial commit

* Include serviceName in crypto key info to allow for better error logging; fix a4a integration test to be hermetic

* Fix test coverage

* PR feedback

* PR responses
Lith pushed a commit to Lith/amphtml that referenced this pull request Dec 22, 2016
…; log signing service to stackdrive with service name (ampproject#6504)

* Initial commit

* Include serviceName in crypto key info to allow for better error logging; fix a4a integration test to be hermetic

* Fix test coverage

* PR feedback

* PR responses
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.

None yet

5 participants