New Fast Fetch signature scheme#9040
New Fast Fetch signature scheme#9040taymonbeal wants to merge 34 commits intoampproject:masterfrom google:new-signature-scheme
Conversation
glevitzky
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| this.iframe = null; | ||
|
|
||
| for (const signingServiceName of this.getSigningServiceNames()) { | ||
| signatureVerifierFor(this.win).loadKeyset(signingServiceName); |
There was a problem hiding this comment.
We call signatureVerifierFor(this.win) multiple times; maybe store it as a local variable instead?
| return win[propertyName]; | ||
| } | ||
|
|
||
| class SignatureVerifier { |
There was a problem hiding this comment.
Please add some comments to the class and its methods describing what it does.
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
Can you maybe add a comment explaining what jwkSet is here?
src/service/crypto-impl.js
Outdated
| /** | ||
| * 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. |
There was a problem hiding this comment.
Is this still true? You aren't calling isCryptoAvailable first
| publicKeyInfo.cryptoKey, | ||
| signature.subarray(5), | ||
| signedData)); | ||
| verifyPkcs(key, signature, data) { |
There was a problem hiding this comment.
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 ... "
| } | ||
| }, | ||
| (err) => { | ||
| dev().error(`Failed to verify signature: ${ |
There was a problem hiding this comment.
nit: if you make a new line before the ${ can the rest all fit on one line?
There was a problem hiding this comment.
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.
bradfrizzell
left a comment
There was a problem hiding this comment.
undoing approval until tests are added
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| export function decodeSignatureHeader(headerValue) { | ||
| if (headerValue) { | ||
| const match = | ||
| new RegExp( |
There was a problem hiding this comment.
Could you add a comment in the jsdoc indicating what the expected format is?
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
|
|
||
| /** | ||
| * Decode the `X-Creativesize` header, in its conventional format, to a size | ||
| * info object. |
There was a problem hiding this comment.
Document expected format for headerValue here as well please
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| export function decodeSizeHeader(headerValue) { | ||
| if (headerValue) { | ||
| dev().assert(new RegExp('[0-9]+x[0-9]+').test(headerValue)); | ||
| const sizeArr = sizeHeader.split('x').map(Number); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
NO_FAULT makes it sound like you are reporting that no fault occurred. Rename to something more accurate
There was a problem hiding this comment.
What do you think it should be called?
There was a problem hiding this comment.
This isn't necessarily the signing service's fault; it could also be caused by the user losing network connectivity.
glevitzky
left a comment
There was a problem hiding this comment.
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 (${ |
There was a problem hiding this comment.
Nit: Not a strong preference, but I think ${var} is more readable than
${
var
}
here and throughout.
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
Nit: Can move this up to previous line if this doesn't screw up the formatting of the subsequent code too much.
There was a problem hiding this comment.
This is because there are two callbacks here, so the rectangle rule requires that they both be on separate lines.
There was a problem hiding this comment.
All formatting in this file was done by clang-format.
| response.headers.get('Content-Type') == | ||
| 'application/jwk-set+json'); | ||
| return response.json().then( | ||
| jwkSet => { |
There was a problem hiding this comment.
Nit: Can move this up to previous line.
| const signaturePromise = | ||
| keypairPromise | ||
| .then( | ||
| ({privateKey}) => subtle.sign( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Nit: Here and elsewhere in this file, we can move the callbacks to the same line as .then.
keithwrightbos
left a comment
There was a problem hiding this comment.
Some initial feedback
| }} */ | ||
| let CreativeMetaDataDef; | ||
| /** @typedef {{width: number, height: number}} */ | ||
| let SizeInfoDef; |
There was a problem hiding this comment.
Is there a base object that already meets this definition (e.g. rect?)?
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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?
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| creativeParts.signatureInfo; | ||
| if (getMode().localDev) { | ||
| // localDev mode allows fake signature for the "fake" network. | ||
| if (signingServiceName == 'FAKESERVICE' && |
There was a problem hiding this comment.
These if clauses can be combined into a single clause
There was a problem hiding this comment.
Can they? I thought getMode().localDev invoked compiler magic that might not happen in that case. Do you know?
There was a problem hiding this comment.
At worst I would expect compiler to just remove that clause. This is done throughout the codebase
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| creative: responseArrayBuffer, | ||
| signatureInfo: decodeSignatureHeader( | ||
| responseHeaders.get('AMP-Fast-Fetch-Signature')), | ||
| sizeInfo: decodeSizeHeader(responseHeaders.get('X-Creativesize')), |
There was a problem hiding this comment.
Is this header name correct? I would have expected the S to be capitalized
There was a problem hiding this comment.
In principle, HTTP header names are case insensitive. But I'll change it anyway.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
| * @return {?SignatureInfoDef} | ||
| */ | ||
| export function decodeSignatureHeader(headerValue) { | ||
| if (headerValue) { |
There was a problem hiding this comment.
Instead of giant if, change to if (!headerValue) return null;
| */ | ||
| fetchAndAddKeys_(keys, signingServiceName, keypairId) { | ||
| let url = signingServerURLs[signingServiceName]; | ||
| if (keypairId != null) { |
There was a problem hiding this comment.
Why not just if (keyPairId)?
| if (keypairId != null) { | ||
| url += '?' + encodeURIComponent(keypairId); | ||
| } | ||
| return xhrFor(this.win_) |
There was a problem hiding this comment.
Make this.xhr_ field
| {mode: 'cors', method: 'GET', ampCors: false, credentials: 'omit'}) | ||
| .then( | ||
| response => { | ||
| if (response.status == 200) { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
Why add space? Did lint complain?
| } | ||
| </script> | ||
| </body></html>`, | ||
| </body></html>\0`, |
There was a problem hiding this comment.
@taymonbeal looks like closure ompiler is choking on this \0
There was a problem hiding this comment.
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
There was a problem hiding this comment.
I found a workaround.
| return /** @type {?SignatureInfoDef} */ ({ | ||
| signingServiceName: match[1], | ||
| keypairId: match[2], | ||
| signature: base64DecodeToBytes(match[3]), |
There was a problem hiding this comment.
Not extremely familiar with the codebase, but would errors in decoding surface here implicitly?
There was a problem hiding this comment.
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, |
* Implement new signature verifier (ported from #9040) * Reword confusing JSDoc description * Fix another bad indent * Respond to review comments * Appease the Dread Linter
|
Obsoleted by #11077. |
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).