Skip to content

New Fast Fetch signature scheme#9040

Closed
taymonbeal wants to merge 34 commits intoampproject:masterfrom
google:new-signature-scheme
Closed

New Fast Fetch signature scheme#9040
taymonbeal wants to merge 34 commits intoampproject:masterfrom
google:new-signature-scheme

Conversation

@taymonbeal
Copy link
Copy Markdown
Member

@taymonbeal taymonbeal commented Apr 28, 2017

Fixes #7618; see there for details of what this does and why it's needed.

Not ready to merge yet; still needs tests and documentation (and breaks a lot of existing tests due to API changes).

Copy link
Copy Markdown
Contributor

@glevitzky glevitzky left a comment

Choose a reason for hiding this comment

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

Minor changes requested, but mostly LGTM. I'll wait until all build/test issues are resolved before adding more comments, if necessary.

this.handleResize_(adResponse.size.width, adResponse.size.height);
return Promise.resolve(adResponse);
});
this.ampAnalyticsConfig = extractAmpAnalyticsConfig(responseHeaders);
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.

Did you make the appropriate changes to this utility function? It's possible I might have missed something, but we need those extra arguments to construct the appropriate CSI pings.

this.iframe = null;

for (const signingServiceName of this.getSigningServiceNames()) {
signatureVerifierFor(this.win).loadKeyset(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.

We call signatureVerifierFor(this.win) multiple times; maybe store it as a local variable instead?

Copy link
Copy Markdown
Contributor

@bradfrizzell bradfrizzell left a comment

Choose a reason for hiding this comment

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

Looks good to me, just have some comments about style/documentation. I'm not that familiar with the existing crypto stuff so take my lgtm with a grain of salt

return win[propertyName];
}

class SignatureVerifier {
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.

Please add some comments to the class and its methods describing what it does.

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.

Working on adding a lot more documentation and tests to everything; it'll be up soon.

(response) => {
if (response.status == 200) {
return response.json().then(
(jwkSet) => {
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 maybe add a comment explaining what jwkSet is here?

/**
* Convert a JSON Web Key object to a browser-native cryptographic key and
* compute a hash for it. The caller must verify that Web Cryptography is
* available using isCryptoAvailable before calling this function.
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.

Is this still true? You aren't calling isCryptoAvailable first

publicKeyInfo.cryptoKey,
signature.subarray(5),
signedData));
verifyPkcs(key, signature, data) {
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 change the jsdoc so it documents what this stands for? Is it Public key crypto signature? i.e.

"Verifies public key crypto signature ... "

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.

"PKCS" refers to the PKCS 1 algorithm.

}
},
(err) => {
dev().error(`Failed to verify signature: ${
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: if you make a new line before the ${ can the rest all fit on one 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.

This is clang-format's fault. Apparently other people within Google have also complained about this behavior. I am not totally sure what the right thing to do about it is, since I don't want to have something that the autoformatter will just revert later.

Copy link
Copy Markdown
Contributor

@bradfrizzell bradfrizzell left a comment

Choose a reason for hiding this comment

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

undoing approval until tests are added

export function decodeSignatureHeader(headerValue) {
if (headerValue) {
const match =
new RegExp(
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.

Could you add a comment in the jsdoc indicating what the expected format is?


/**
* Decode the `X-Creativesize` header, in its conventional format, to a size
* info object.
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.

Document expected format for headerValue here as well please

export function decodeSizeHeader(headerValue) {
if (headerValue) {
dev().assert(new RegExp('[0-9]+x[0-9]+').test(headerValue));
const sizeArr = sizeHeader.split('x').map(Number);
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.

is this supposed to be headerValue?

* A network connectivity failure, misbehaving signing service, or other
* factor beyond the ad network's control caused verification to fail.
*/
NO_FAULT: 1,
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.

NO_FAULT makes it sound like you are reporting that no fault occurred. Rename to something more accurate

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.

What do you think it should be called?

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.

SIGNING_SERVICE_FAILURE?

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.

This isn't necessarily the signing service's fault; it could also be caused by the user losing network connectivity.

Copy link
Copy Markdown
Contributor

@glevitzky glevitzky left a comment

Choose a reason for hiding this comment

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

Most of my feedback is just minor formatting stuff. If you feel that any of it will make the code less readable, feel free to ignore the comment.

const message = err && err.message;
signingServiceError(
signingServiceName,
`Failed to import key (${
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: Not a strong preference, but I think ${var} is more readable than

${
  var
}

here and throughout.

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.

I agree, but clang-format doesn't. I'm hesitant to contradict it because that might result in the line getting reformatted later.

return cryptoFor(this.win_)
.verifyPkcs(key, signature, creative)
.then(
result => {
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: Can move this up to previous line if this doesn't screw up the formatting of the subsequent code too much.

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.

This is because there are two callbacks here, so the rectangle rule requires that they both be on separate lines.

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.

All formatting in this file was done by clang-format.

response.headers.get('Content-Type') ==
'application/jwk-set+json');
return response.json().then(
jwkSet => {
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: Can move this up to previous line.

const signaturePromise =
keypairPromise
.then(
({privateKey}) => subtle.sign(
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: move to previous line.

it('should validate with the correct key and signature', () => {
return publicJwkPromise.then(jwk => crypto.importPkcsKey(jwk))
.then(
key => signaturePromise.then(
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: Here and elsewhere in this file, we can move the callbacks to the same line as .then.

Copy link
Copy Markdown
Contributor

@keithwrightbos keithwrightbos left a comment

Choose a reason for hiding this comment

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

Some initial feedback

}} */
let CreativeMetaDataDef;
/** @typedef {{width: number, height: number}} */
let SizeInfoDef;
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.

Is there a base object that already meets this definition (e.g. rect?)?

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.

If there is, we weren't using it. I just factored this out into a separate typedef to make Closure stop complaining.

this.fromResumeCallback = false;

const signatureVerifier = signatureVerifierFor(this.win);
this.getSigningServiceNames().forEach(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.

I realize we currently fetch public keys in the constructor but should this be done instead in buildCallback? Should it be blocked by not in prerender state?

creativeParts.signatureInfo;
if (getMode().localDev) {
// localDev mode allows fake signature for the "fake" network.
if (signingServiceName == 'FAKESERVICE' &&
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.

These if clauses can be combined into a single clause

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.

Can they? I thought getMode().localDev invoked compiler magic that might not happen in that case. Do you know?

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.

At worst I would expect compiler to just remove that clause. This is done throughout the codebase

creative: responseArrayBuffer,
signatureInfo: decodeSignatureHeader(
responseHeaders.get('AMP-Fast-Fetch-Signature')),
sizeInfo: decodeSizeHeader(responseHeaders.get('X-Creativesize')),
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.

Is this header name correct? I would have expected the S to be capitalized

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.

In principle, HTTP header names are case insensitive. But I'll change it anyway.

* @return {?SignatureInfoDef}
*/
export function decodeSignatureHeader(headerValue) {
if (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.

Instead of giant if, change to if (!headerValue) return null;

*/
fetchAndAddKeys_(keys, signingServiceName, keypairId) {
let url = signingServerURLs[signingServiceName];
if (keypairId != 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 not just if (keyPairId)?

if (keypairId != null) {
url += '?' + encodeURIComponent(keypairId);
}
return xhrFor(this.win_)
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.

Make this.xhr_ field

{mode: 'cors', method: 'GET', ampCors: false, credentials: 'omit'})
.then(
response => {
if (response.status == 200) {
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, code is more readable if you have if (response.status != 200) {... }

if (response.status == 200) {
// The signing service spec requires this, but checking it at
// runtime in production isn't worth it.
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.

Why only dev? This could definitely occur in production and so we should guard against it, no?

if (this.ampAnalyticsConfig_) {
// Load amp-analytics extensions
this.extensions_./*OK*/loadExtension('amp-analytics');
this.extensions_./*OK*/ loadExtension('amp-analytics');
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 add space? Did lint complain?

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.

clang-format did it.

}
</script>
</body></html>`,
</body></html>\0`,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@taymonbeal looks like closure ompiler is choking on this \0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if this file isnt source code and just for tests, prefix it with test- or suffix with _test so that it isnt globbed by the compiler task

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.

I found a workaround.

return /** @type {?SignatureInfoDef} */ ({
signingServiceName: match[1],
keypairId: match[2],
signature: base64DecodeToBytes(match[3]),
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.

Not extremely familiar with the codebase, but would errors in decoding surface here implicitly?

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.

Decoding errors shouldn't be possible, because the SIGNATURE_FORMAT regex should only match valid base64 strings.

* A network connectivity failure, misbehaving signing service, or other
* factor beyond the ad network's control caused verification to fail.
*/
NO_FAULT: 1,
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.

SIGNING_SERVICE_FAILURE?

keithwrightbos pushed a commit that referenced this pull request Aug 25, 2017
* Implement new signature verifier (ported from #9040)

* Reword confusing JSDoc description

* Fix another bad indent

* Respond to review comments

* Appease the Dread Linter
@taymonbeal
Copy link
Copy Markdown
Member Author

Obsoleted by #11077.

@taymonbeal taymonbeal closed this Aug 29, 2017
@taymonbeal taymonbeal deleted the new-signature-scheme branch August 29, 2017 19:57
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.

7 participants