Bug 1771992 - Use stage cert depending on the configured GMP URL r=aosmond,bhearsum

Differential Revision: https://phabricator.services.mozilla.com/D205637
This commit is contained in:
Rob Wu 2024-03-26 14:53:36 +00:00
parent 33c7e57530
commit 7bbc54b70e
4 changed files with 120 additions and 20 deletions

View file

@ -161,6 +161,36 @@ GMPInstallManager.prototype = {
return url;
},
/**
* Determines the root to use for verifying content signatures.
* @param url
* The Balrog URL, i.e. the return value of _getURL().
*/
_getContentSignatureRootForURL(url) {
// The prod and stage URLs of Balrog are documented at:
// https://mozilla-balrog.readthedocs.io/en/latest/infrastructure.html
// Note: we are matching by prefix without the full domain nor slash, to
// enable us to move to a different host name in the future if desired.
if (url.startsWith("https://aus")) {
return Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot;
}
if (url.startsWith("https://stage.")) {
return Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot;
}
if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
return Ci.nsIX509CertDB.AppXPCShellRoot;
}
// When content signature verification for GMP was added (bug 1714621), a
// pref existed to configure an arbitrary root, which enabled local testing.
// This pref was removed later in bug 1769669, and replaced with hard-coded
// roots (prod and tests only). Support for testing against the stage server
// was restored in bug 1771992.
// Note: other verifiers ultimately fall back to ContentSignatureLocalRoot,
// to support local development. Here we use ContentSignatureProdRoot to
// minimize risk (and the unclear demand for "local" development).
return Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot;
},
/**
* Records telemetry results on if fetching update.xml from Balrog succeeded
* when content signature was used to verify the response from Balrog.
@ -335,9 +365,10 @@ GMPInstallManager.prototype = {
}
let url = await this._getURL();
let trustedContentSignatureRoot = this._getContentSignatureRootForURL(url);
log.info(
`Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}`
`Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}, trustedContentSignatureRoot=${trustedContentSignatureRoot}`
);
let success = true;
@ -347,7 +378,8 @@ GMPInstallManager.prototype = {
url,
allowNonBuiltIn,
certs,
checkContentSignature
checkContentSignature,
trustedContentSignatureRoot
);
if (checkContentSignature) {

View file

@ -836,6 +836,60 @@ add_task(async function test_checkForAddons_contentSignatureFailure() {
revertContentSigTestPrefs(previousUrlOverride);
});
/**
* Tests that the signature verification URL is as expected.
*/
add_task(async function test_checkForAddons_get_verifier_url() {
const previousUrlOverride = setupContentSigTestPrefs();
let installManager = new GMPInstallManager();
// checkForAddons() calls _getContentSignatureRootForURL() with the return
// value of _getURL(), which is effectively KEY_URL_OVERRIDE or KEY_URL
// followed by some normalization.
const rootForUrl = async () => {
const url = await installManager._getURL();
return installManager._getContentSignatureRootForURL(url);
};
Assert.equal(
await rootForUrl(),
Ci.nsIX509CertDB.AppXPCShellRoot,
"XPCShell root used by default in xpcshell test"
);
const defaultPrefs = Services.prefs.getDefaultBranch("");
const defaultUrl = defaultPrefs.getStringPref(GMPPrefs.KEY_URL);
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, defaultUrl);
Assert.equal(
await rootForUrl(),
Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot,
"Production cert should be used for the default Balrog URL: " + defaultUrl
);
// The current Balrog endpoint is at aus5.mozilla.org. Confirm that the prod
// cert is used even if we bump the version (e.g. aus6):
const potentialProdUrl = "https://aus1337.mozilla.org/potential/prod/URL";
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, potentialProdUrl);
Assert.equal(
await rootForUrl(),
Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot,
"Production cert should be used for: " + potentialProdUrl
);
// Stage URL documented at https://mozilla-balrog.readthedocs.io/en/latest/infrastructure.html
const stageUrl = "https://stage.balrog.nonprod.cloudops.mozgcp.net/etc.";
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, stageUrl);
Assert.equal(
await rootForUrl(),
Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot,
"Stage cert should be used with the stage URL: " + stageUrl
);
installManager.uninit();
revertContentSigTestPrefs(previousUrlOverride);
});
/**
* Tests that checkForAddons() works as expected when certificate pinning
* checking is enabled. We plan to move away from cert pinning in favor of

View file

@ -118,12 +118,18 @@ async function conservativeFetch(input) {
* @param contentSignatureHeader
* The contents of the 'content-signature' header received along with
* `data`.
* @param trustedRoot
* The identifier of the trusted root to use for certificate validation.
* @return A promise that will resolve to nothing if the signature verification
* succeeds, or rejects on failure, with an Error that sets its
* addonCheckerErr property disambiguate failure cases and a message
* explaining the error.
*/
async function verifyGmpContentSignature(data, contentSignatureHeader) {
async function verifyGmpContentSignature(
data,
contentSignatureHeader,
trustedRoot
) {
if (!contentSignatureHeader) {
logger.warn(
"Unexpected missing content signature header during content signature validation"
@ -186,13 +192,6 @@ async function verifyGmpContentSignature(data, contentSignatureHeader) {
"@mozilla.org/security/contentsignatureverifier;1"
].createInstance(Ci.nsIContentSignatureVerifier);
// See bug 1771992. In the future, this may need to handle staging and dev
// environments in addition to just production and testing.
let root = Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot;
if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
root = Ci.nsIX509CertDB.AppXPCShellRoot;
}
let valid;
try {
valid = await verifier.asyncVerifyContentSignature(
@ -200,7 +199,7 @@ async function verifyGmpContentSignature(data, contentSignatureHeader) {
signature,
certChain,
"aus.content-signature.mozilla.org",
root
trustedRoot
);
} catch (err) {
logger.warn(`Unexpected error while validating content signature: ${err}`);
@ -329,6 +328,9 @@ function downloadXMLWithRequest(
* @param verifyContentSignature
* When true, will verify the content signature information from the
* response header. Failure to verify will result in an error.
* @param trustedContentSignatureRoot
* The trusted root to use for certificate validation.
* Must be set if verifyContentSignature is true.
* @return a promise that resolves to the DOM document downloaded or rejects
* with a JS exception in case of error.
*/
@ -336,7 +338,8 @@ async function downloadXML(
url,
allowNonBuiltIn = false,
allowedCerts = null,
verifyContentSignature = false
verifyContentSignature = false,
trustedContentSignatureRoot = null
) {
let request = await downloadXMLWithRequest(
url,
@ -346,7 +349,8 @@ async function downloadXML(
if (verifyContentSignature) {
await verifyGmpContentSignature(
request.response,
request.getResponseHeader("content-signature")
request.getResponseHeader("content-signature"),
trustedContentSignatureRoot
);
}
return request.responseXML;
@ -535,6 +539,9 @@ export const ProductAddonChecker = {
* @param verifyContentSignature
* When true, will verify the content signature information from the
* response header. Failure to verify will result in an error.
* @param trustedContentSignatureRoot
* The trusted root to use for certificate validation.
* Must be set if verifyContentSignature is true.
* @return a promise that resolves to an object containing the list of add-ons
* and whether the local fallback was used, or rejects with a JS
* exception in case of error. In the case of an error, a best effort
@ -545,13 +552,15 @@ export const ProductAddonChecker = {
url,
allowNonBuiltIn = false,
allowedCerts = null,
verifyContentSignature = false
verifyContentSignature = false,
trustedContentSignatureRoot = null
) {
return downloadXML(
url,
allowNonBuiltIn,
allowedCerts,
verifyContentSignature
verifyContentSignature,
trustedContentSignatureRoot
).then(parseXML);
},

View file

@ -98,7 +98,8 @@ add_task(async function test_valid_content_signature() {
signedBaseUri + goodXmlPath + "?" + validSignatureQuery,
/*allowNonBuiltIn*/ false,
/*allowedCerts*/ false,
/*verifyContentSignature*/ true
/*verifyContentSignature*/ true,
/*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot
);
Assert.ok(true, "Should successfully get addon list");
@ -122,7 +123,8 @@ add_task(async function test_invalid_content_signature() {
signedBaseUri + goodXmlPath + "?" + invalidSignatureQuery,
/*allowNonBuiltIn*/ false,
/*allowedCerts*/ false,
/*verifyContentSignature*/ true
/*verifyContentSignature*/ true,
/*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot
);
Assert.ok(false, "Should fail to get addon list");
} catch (e) {
@ -143,7 +145,8 @@ add_task(async function test_missing_content_signature_header() {
signedBaseUri + goodXmlPath + "?" + missingSignatureQuery,
/*allowNonBuiltIn*/ false,
/*allowedCerts*/ false,
/*verifyContentSignature*/ true
/*verifyContentSignature*/ true,
/*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot
);
Assert.ok(false, "Should fail to get addon list");
} catch (e) {
@ -165,7 +168,8 @@ add_task(async function test_incomplete_content_signature_header() {
signedBaseUri + goodXmlPath + "?" + incompleteSignatureQuery,
/*allowNonBuiltIn*/ false,
/*allowedCerts*/ false,
/*verifyContentSignature*/ true
/*verifyContentSignature*/ true,
/*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot
);
Assert.ok(false, "Should fail to get addon list");
} catch (e) {
@ -187,7 +191,8 @@ add_task(async function test_bad_x5u_content_signature_header() {
signedBaseUri + goodXmlPath + "?" + badX5uSignatureQuery,
/*allowNonBuiltIn*/ false,
/*allowedCerts*/ false,
/*verifyContentSignature*/ true
/*verifyContentSignature*/ true,
/*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot
);
Assert.ok(false, "Should fail to get addon list");
} catch (e) {