Skip to content

Implement new signature verifier#11077

Merged
keithwrightbos merged 5 commits intoampproject:masterfrom
google:new-signature-verifier
Aug 25, 2017
Merged

Implement new signature verifier#11077
keithwrightbos merged 5 commits intoampproject:masterfrom
google:new-signature-verifier

Conversation

@taymonbeal
Copy link
Copy Markdown
Member

At long last, the new verifier arrives!

...sort of. This change introduces the code for the new verifier into the repo, but does not cause it to be compiled into the Fast Fetch extensions in production. The actual SignatureVerifier class is dead code and should be optimized out by the compiler. It is just here so that unit tests can be run against it, and to facilitate the next PR which links it into the Fast Fetch extensions for real, as an experiment.

Replaces #9040 (the core logic from that PR is in here). Related to #7618. Follow-up to #10674.

@taymonbeal taymonbeal requested a review from glevitzky August 24, 2017 15:32
*
* Unlike an AMP service, a signature verifier is **stateful**. It maintains a
* cache of all public keys that it has previously downloaded and imported, and
* also keeps track of which keys and signing services have already had failed
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: "have already had failed" -> "have already failed".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

"Failed" is actually an adjective here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Will reword though.

}
return success;
});
// This "recursive" call can recurse at most once.
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.

For my own understanding, can you explain why this can only recurse once?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Look seven lines up. The new signer.promise ensures that signer.keys[keypairId] can't be undefined again, by setting it to null if it is. So the second call can't go down this branch.

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.

Got it, thanks.

return;
}

describe('when crypto is available', () => {
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.

Can you add a test that ensures verifyCreativeAndSignature bottoms out after at most 1 recursive call? (Unless I missed it.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That verifyCreativeAndSignature calls itself is really an implementation detail to make the code more concise, so I think a test that spied on it directly would be overly fragile. It seems like what we really want to assert is that the verifier will not make more than two HTTP requests for a single kid; for that, see should not make more network requests retrying a nonexistent kid.

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.

SGTM.

}
return Services.xhrFor(this.win_)
.fetchJson(
url, // eslint-disable-line indent
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.

This is because it should be on the previous line.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why should it be on the previous line when the next argument can't be? Doesn't that violate the rectangle rule?

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.

If the first argument is on a new line, it's only indented two spaces (it's not a line continuation).

Copy link
Copy Markdown
Member Author

@taymonbeal taymonbeal Aug 24, 2017

Choose a reason for hiding this comment

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

Style guide disagrees. Can we fix the tooling?

// - Chain cancelled => don't return; drop error
// - Uncaught error otherwise => don't return; percolate error up
this.adPromise_ = Services.viewerForDoc(this.getAmpDoc()).whenFirstVisible()
this.adPromise_ = this.keysetPromise_
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.

As discussed f2f, please revert this back to whenFirstVisible to ensure that keyset fetch failure does not prevent ad request. keySetPromise_ should be a dependency only on verifying responses with signature.

// Web Cryptography isn't available.
return Promise.resolve(VerificationStatus.UNVERIFIED);
}
const signer = this.signers_[signingServiceName];
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.

Add a dev assert that signer exists

return Promise.resolve(VerificationStatus.UNVERIFIED);
}
const signer = this.signers_[signingServiceName];
return signer.promise.then(success => {
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 we limit the amount of time we're willing to wait for the public key? Presumably there is some performance trade-off (perhaps add a TODO).

const match = signatureFormat.exec(headerValue);
if (!match) {
// TODO(@taymonbeal, #9274): replace this with real error reporting
user().error('AMP-A4A', `Invalid signature header: ${headerValue}`);
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.

Do we really need to send the entire header value? Seems unnecessary... how about just the signing service name?

});
return VerificationStatus.OK;
} else {
return VerificationStatus.ERROR_SIGNATURE_MISMATCH;
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 prefer we send a CSI ping in both cases... certainly it would be of value so we could track how often client-validation fails but perhaps that's out of scope for this PR. At last add a TODO.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See #11090 and tell me what you think?

return Services.xhrFor(this.win_)
.fetchJson(
url, // eslint-disable-line indent
{mode: 'cors', method: 'GET', ampCors: false, credentials: 'omit'})
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.

We used to have a nice comment about why we do ampCors false... can it come back?

// and there's no meaningful error recovery to be done if they
// fail, so we don't need to do them at runtime in production.
// They're included in dev mode as a debugging aid.
dev().assert(
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 believe fetchJson already does this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not quite. You could send 204 and fetchJson would let it through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants