Skip to content

Origin trials#8944

Merged
kmh287 merged 14 commits intoampproject:masterfrom
kmh287:origin_trials
May 26, 2017
Merged

Origin trials#8944
kmh287 merged 14 commits intoampproject:masterfrom
kmh287:origin_trials

Conversation

@kmh287
Copy link
Copy Markdown
Contributor

@kmh287 kmh287 commented Apr 25, 2017

Partial for #8331.

Allows publishers to enable certain experiments, provided that a page includes a token that describes the experiments that should be enabled. The token is formatted as follows:

A base64 encoding of:

  • 1 byte version number, starting at 0
  • 4 byte length of experiments config
  • The experiments config, as a JSON
  • The signature of the experiments JSON

The experiments config will contain the experiments to enable, their expirations, and the origin to enable the experiments for. (To prevent copying of one token from one site to another).

/to @choumx @jridgewell

Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 left a comment

Choose a reason for hiding this comment

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


function getExperimentsKey_() {
let key;
const url = '' //TODO(kmh287): fill this in
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.

Not sure how to set up a key to download. Is the extra request worth the time it would take to perform another request?

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 don't think we want to do that since it'll prevent offline functionality. We can patch prod if we really need to kill a feature, though I don't think that will be needed in practice.

method: 'GET',
ampCors: false,
credentials: 'omit',
}).then(jwkObj => {
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.

How can this be tested? I'm not sure how to generate a JSON web key nor how to use it to sign some fake data.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Give this site a shot.

* @param {string} experimentId
* @return {!Promise<boolean>}
*/
export function isExperimentOnForOriginTrial(win, experimentId) {
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.

Should this function just run once when the module is loaded?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should be up to the caller. May need to be run multiple times in the case where there are multiple AMP documents (e.g. shadow AMPs) but we should cache the result.

throw new Error('Config does not match current origin');;
}
const experiments = config['experiments'];
const experiment = experiments[experimentId];
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.

When we generate these tokens, we're going to need to either keep track of all experiments for each origin on our end, or request that all experiments be requested at once. Alternatively we can allow multiple meta tags with tokens, but that sounds like it could get slow very quickly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ack. Publishers that want to be whitelisted for additional experiments will receive a new token that covers old and new experiments.

Copy link
Copy Markdown

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

Good start! I'll kick off an email thread with the A4A team who owns this code to ask for guidance on usage of this API.


function getExperimentsKey_() {
let key;
const url = '' //TODO(kmh287): fill this in
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 don't think we want to do that since it'll prevent offline functionality. We can patch prod if we really need to kill a feature, though I don't think that will be needed in practice.

method: 'GET',
ampCors: false,
credentials: 'omit',
}).then(jwkObj => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Give this site a shot.

* @param {string} experimentId
* @return {!Promise<boolean>}
*/
export function isExperimentOnForOriginTrial(win, experimentId) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should be up to the caller. May need to be run multiple times in the case where there are multiple AMP documents (e.g. shadow AMPs) but we should cache the result.

throw new Error('Config does not match current origin');;
}
const experiments = config['experiments'];
const experiment = experiments[experimentId];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ack. Publishers that want to be whitelisted for additional experiments will receive a new token that covers old and new experiments.

return false;
}
// TODO(kmh287): Transient experiment?
toggleExperiment(win, experimentId, /* opt_on */ true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We shouldn't do this since origin trials have expiry dates. We want the feature to be disabled when the expiry date passes (unless the user has explicitly opted-in).

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.

Doesn't the check on the previous few lines handle that?

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 really since the publisher could just remove the token.

Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 May 2, 2017

Choose a reason for hiding this comment

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

Oh, because the experiment toggle is saved in a cookie by default. Then why not just toggle it as a transient experiment?

@googlebot
Copy link
Copy Markdown

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@googlebot googlebot added cla: no and removed cla: yes labels May 3, 2017
Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 left a comment

Choose a reason for hiding this comment

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

Will, I've updated the PR with some tests on tokens I generated. I've got a script I threw together that generates the tokens, but there were a few complications

  1. The crypto service expects a modified signature format.
  2. The crypto service expects the signature to have been generated by signing modified data

I had to account for both when generating the tokens.

@googlebot
Copy link
Copy Markdown

CLAs look good, thanks!

@googlebot googlebot added cla: yes and removed cla: no labels May 3, 2017
Copy link
Copy Markdown

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

Nice! Is this still [WIP]? If not we can remove and ask for review from one of the A4A guys.

*/
const bytes = stringToBytes(atob(token));
const version = bytes[0];
if (version === 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.

Minor: Early out here to reduce brace nesting below.

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, though we'll need to revert if another version is added

/**
* Version 0:
* length = 4 bytes representing number of bytes in config
* config = string containing the experiment ID, origin URL, etc.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Readability suggestion: Use separate vars for each magic number, e.g.

let current = 1;
const bytesForConfigSize = 4;

// Parse config size.
const bytesForConfig = bytesToInt(bytes.subarray(current, bytesForConfigSize));
current += bytesForConfigSize;

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


/**
* Whether the specified experiment is on or off for origin trials. You should
* check if the experiment is already enabled before calling this function.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Describe when/how the promise is rejected.

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, let me know if you think it's too much detail

throw new Error('Failed to verify config signature');
}
const configStr = utf8DecodeSync(configBytes);
const config = JSON.parse(configStr);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This can throw errors on invalid JSON. Is the default error message good enough? If not, we have tryParseJson.

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 should only fail if we generated a bad token, or if the token has been modified. Do you think we should catch the error, add more context, and then reject?

const url = ampdocServiceFor(win).getAmpDoc().getUrl();
const sourceOrigin = getSourceOrigin(url);
if (approvedOrigin !== sourceOrigin) {
throw new Error('Config does not match current origin');;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Extra ;

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.

thanks

* Assumes bytes are big endian.
* @param {!Uint8Array} bytes
* @return {number}
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please add a unit test for this method.

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.

'AAAAAJF7Im9yaWdpbiI6Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20iLCJleHBlcmltZW50' +
'cyI6eyJhbXAtZXhwaXJlcy1sYXRlciI6eyJleHBpcmF0aW9uIjo5NTYxNzYwMjAwMDAw' +
'MH0sImFtcC1leHBpcmVkLSI6eyJleHBpcmF0aW9uIjoxMjMyNDI3NjAwMDAwfX19YmFk' +
'c2lnbmF0dXJlISEhISEh';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

publicJwk and the tokens need to be in beforeEach?

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.

nah, fixed

beforeEach(() => {
win = env.win;
ampdoc = env.ampdoc;
sandbox = sinon.sandbox.create();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

describes.js provides env.sandbox.

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

// origin: 'https://www.google.com',
// experiments: {
// 'amp-expires-later': {expiration: 95617602000000},
// 'amp-expired-': {expiration: 1232427600000},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'amp-expired'

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, thanks

});

it('should throw for missing token', () => {
if (!crypto.isCryptoAvailable()) { return; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Extract this to wrap it methods instead? Probably better to not run than appear to 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.

I think this is how the crypto tests are current setup. (not that that's the best solution here) What do you mean by wrap the it methods?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if (crypto.isCryptoAvailable()) {
  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.

crypto isn't setup until the beforeEach runs. Besides, I don't think this is really an issue as the case where crypto is unavailable is already tested.

@dreamofabear
Copy link
Copy Markdown

The crypto service expects a modified signature format.
The crypto service expects the signature to have been generated by signing modified data

Interesting, looks like an optimization. Let's ask A4A -- maybe we can use this too.

@kmh287 kmh287 mentioned this pull request May 8, 2017
Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 left a comment

Choose a reason for hiding this comment

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

@choumx Thanks for being so thorough. Accepted most of your comments but responded to a few. When you think it's good to send out to other reviewers, let me know who you recommend and I'll remove the WIP tags.


/**
* Whether the specified experiment is on or off for origin trials. You should
* check if the experiment is already enabled before calling this function.
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, let me know if you think it's too much detail

*/
const bytes = stringToBytes(atob(token));
const version = bytes[0];
if (version === 0) {
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, though we'll need to revert if another version is added

/**
* Version 0:
* length = 4 bytes representing number of bytes in config
* config = string containing the experiment ID, origin URL, etc.
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

* @param {!Object} opt_publicJwk Used for testing only.
* @return {!Promise<boolean>}
*/
export function isExperimentOnForOriginTrial(win, experimentId, opt_publicJwk) {
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.

@choumx Do you think there's a better way to provide the public key for testing besides the optional third param here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Depends on how the actual public key is stored. OK for now.

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.

I imagine anyone able to modify the public key (and therefore use tokens they signed themselves) could also just do AMP.toggleExperiment, right?

throw new Error('Failed to verify config signature');
}
const configStr = utf8DecodeSync(configBytes);
const config = JSON.parse(configStr);
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 should only fail if we generated a bad token, or if the token has been modified. Do you think we should catch the error, add more context, and then reject?

* Assumes bytes are big endian.
* @param {!Uint8Array} bytes
* @return {number}
*/
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.

beforeEach(() => {
win = env.win;
ampdoc = env.ampdoc;
sandbox = sinon.sandbox.create();
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

// origin: 'https://www.google.com',
// experiments: {
// 'amp-expires-later': {expiration: 95617602000000},
// 'amp-expired-': {expiration: 1232427600000},
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, thanks

'AAAAAJF7Im9yaWdpbiI6Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20iLCJleHBlcmltZW50' +
'cyI6eyJhbXAtZXhwaXJlcy1sYXRlciI6eyJleHBpcmF0aW9uIjo5NTYxNzYwMjAwMDAw' +
'MH0sImFtcC1leHBpcmVkLSI6eyJleHBpcmF0aW9uIjoxMjMyNDI3NjAwMDAwfX19YmFk' +
'c2lnbmF0dXJlISEhISEh';
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.

nah, fixed

});

it('should throw for missing token', () => {
if (!crypto.isCryptoAvailable()) { return; }
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.

I think this is how the crypto tests are current setup. (not that that's the best solution here) What do you mean by wrap the it methods?

Copy link
Copy Markdown

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

Can you follow-up on that email thread with two questions:

  • Why the hash in crypto-impl.js and should we use it?
  • Storage recommendation for private keys?

});

it('should throw for missing token', () => {
if (!crypto.isCryptoAvailable()) { return; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if (crypto.isCryptoAvailable()) {
  it(...);
}

* config = string containing the experiment ID, origin URL, etc.
*/
const bytesForConfigSize = 4;
const configLen =
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: configSize to be consistent, or change the other var to bytesForConfigLen.

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

const config = JSON.parse(configStr);

const approvedOrigin = config['origin'];
const url = ampdocServiceFor(win).getAmpDoc().getUrl();
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 means that a document (e.g. shadow AMP) could enable experiments for the window. We probably should just use win.location as amp-experiments-opt-in does.

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.

ok

const approvedOrigin = config['origin'];
const url = ampdocServiceFor(win).getAmpDoc().getUrl();
const sourceOrigin = getSourceOrigin(url);
if (approvedOrigin !== sourceOrigin) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there an issue with optional trailing slash?

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.

I believe Location.origin always returns without a trailing slash. We'll need to make sure we create and sign the tokens that way.

* @param {!Object} opt_publicJwk Used for testing only.
* @return {!Promise<boolean>}
*/
export function isExperimentOnForOriginTrial(win, experimentId, opt_publicJwk) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Depends on how the actual public key is stored. OK for now.

if (token) {
meta.setAttribute('content', token);
}
win.document.head.appendChild(meta);
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 assume this is automatically torn down after each unit test?

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.

looks like

const experiment = experiments[experimentId];
if (!experiment) {
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Now I think of it -- for two origin experiments A and B, we probably want to allow publishers to have the flexibility to choose to enable origin trial A && !B on some pages. Chrome also only has one experiment per token.

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.

Then we should probably run this at page load. Otherwise we will need to apply this process to every token on the page. We could do it once lazily, but then we could still have a promise that takes a very long time to resolve.

WDYT?

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 should all be pre-evaluated, that way error reporting can log that each experiment is enabled.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SGTM

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, PTAL

/**
* Determines if the specified experiment is on or off for origin trials.
* Callers shouldcheck if the experiment is already enabled before calling this
* function.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since we're just using a transient experiment, we can check this internally. This should avoid parsing a token more than once.

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.

ok

* config = string containing the experiment ID, origin URL, etc.
*/
const bytesForConfigSize = 4;
const configSize =
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.

All of this byte array stuff is way more complicated than necessary. Why not just use a comma delimited string?

${VERSION = 0},${BASE_64},${SIGNATURE}

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.

@choumx thoughts?

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 can decide on the actual delimiter (commas are used to separate Origin-Trial headers), but anything will do. We have the benefit of not requiring super-high-performance can-never-exhaust-memory strings.

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.

That may be a way to go, considering that the version number, base64 encoded experiments object, and signature can't contain commas.

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 believe including the length of the config prevents length extension attack.

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.

In that case, we would need to sign the length, too. I still don't see this as a high priority, though.


const expiration = experiment['expiration'];
const now = Date.now();
if (expiration < now) {
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 need to ensure expiration is in milliseconds, or we need to normalize now into seconds.

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.

We control the contents of the token, so we can do the former.

const experiment = experiments[experimentId];
if (!experiment) {
return 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.

These should all be pre-evaluated, that way error reporting can log that each experiment is enabled.

@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented May 10, 2017

@choumx The hash in crypto-impl is definitely there to bail out of verification early. We don't really have a choice but to include it, unless we want to add similar methods to crypto-impl that don't require it. I'll ask them about recommendations for private key storage.

EDIT: There would be no need to modify cryppto-impl. If we wanted to get around the hash and version number additions to the signature and version number addition to the pre-signed data, we should just use the browser's crypto directly.

* config = string containing the experiment ID, origin URL, etc.
*/
const bytesForConfigSize = 4;
const configSize =
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 believe including the length of the config prevents length extension attack.

const approvedOrigin = config['origin'];
const url = win.location;
const sourceOrigin = getSourceOrigin(url);
if (approvedOrigin !== sourceOrigin) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since approvedOrigin is just from unfiltered, user-submitted form data, I still think we should make this robust enough such that inclusion of optional fragments like www. and trailing slash will still work.

Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 May 12, 2017

Choose a reason for hiding this comment

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

I agree about trailing slash, but some of that should be handled when we validate the forms.

const experiment = experiments[experimentId];
if (!experiment) {
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SGTM

@kmh287 kmh287 changed the title [WIP] Origin trials Origin trials May 15, 2017
@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented May 16, 2017

@choumx @jridgewell PTAL

Copy link
Copy Markdown

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

Did you speak with @taymonbeal about the upcoming crypto-impl.js API changes and key rotation strategy?

let toggles_ = null;

/** @type {!Promise} */
const originTrialsPromise = enableExperimentsForOriginTrials(self);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should avoid competing with startup tasks. The non-startup flavor of chunk may be a good fit here.

How long does it take to verify a single token? That should decide the granularity of the chunk.

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.

Any suggestions on how to get that to work with the async nature of the token parsing? enableExperimentsForOriginTrials needs to be able to return a promise.

I profiled enableExperimentsFromToken and at 10x throttling, Chomr reported about 5ms, though I'm dubious. Does that number sound reasonable to you?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You can create a new Promise, store the resolve function and call it manually when it completes.

At 5ms it might be fine to have the entire thing in a single chunk.

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.

But how can chunk control when the different parts of the task run if the task has a long promise chain?

@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented May 16, 2017

I've put something on our calendars for next week. Taymon took a look over this PR and, I'm assuming, didn't see any glaring issues. Reviewing the PR that changes crypto-impl, I can tel I'll need to tweak the token generation process (and, therefore, the test tokens) but the logic here should be unaffected. Of course, I'll test again after that code is merged to be sure.

return crypto.importPublicKey('experiments', publicJwk).then(keyInfo => {
return crypto.verifySignature(configBytes, signatureBytes, keyInfo);
}).then(verified => {
if (!verified) {
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 needs to be an Error instance, and you can just throw 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.

ok


const approvedOrigin = parseUrl(config['origin']).origin;
const sourceOrigin = getSourceOrigin(win.location);
if (approvedOrigin !== sourceOrigin) {
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.

Ditto.

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.

ok

if (approvedOrigin !== sourceOrigin) {
return Promise.reject('Config does not match current origin');
}

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.

Multiple experiments per token breaks with Chrome's Origin Trials.

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.

can you link or send me info on that?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 don't understand the argument. Any two trials can be turned on independently, accomplishing #8944 (comment).

const configBytes = bytes.subarray(current, current + configSize);
current += configSize;
const signatureBytes = bytes.subarray(current);

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.

TODO

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 may need to be filled in later once we determine the proper way to store the key pair.

user().warn(TAG, 'Crypto is unavailable');
return Promise.resolve();
}
/**
Copy link
Copy Markdown

@dreamofabear dreamofabear May 16, 2017

Choose a reason for hiding this comment

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

Continuing from: #8944 (comment)

Should be sign(version + length + config, private_key). My mistake on the doc.

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

Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 left a comment

Choose a reason for hiding this comment

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

Changed to one experiment per token and updated the signature verification.

user().warn(TAG, 'Crypto is unavailable');
return Promise.resolve();
}
/**
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

export function bytesToUInt32(bytes) {
if (bytes.length != 4) {
throw new Error('Received byte array with length != 4');
}
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: We can shorten this a bit by forcing 8 bit ints before shifting:

const val = (bytes[0] & 0xFF) << 24 |
     (bytes[1] & 0xFF) << 16 |
     (bytes[2] & 0xFF) << 8 |
     (bytes[3] & 0xFF);

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

…ivate key were disclosed and we needed to invalidate all tokens
@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented May 26, 2017

@choumx

Copy link
Copy Markdown

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

One change and then let's get this in. Do you have the token generation steps in a shared doc?

//TODO(kmh287, #8331) Replaced empty object literal with real experiment public
// key jwk.
/** @type {!Promise} */
const originTrialsPromise = enableExperimentsForOriginTrials(self, {});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's comment this out for now pending resolution of the startup latency issue.

@kmh287 kmh287 merged commit 438575b into ampproject:master May 26, 2017
@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented May 26, 2017

I sent you a js file a few days ago.

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.

5 participants