Skip to content

Commit e536e1e

Browse files
shahzaibjshjameelCopilot
authored
Fix Edge browser selection for multi-signer APKs (MSAL #2414), Fixes AB#3611725 (#3126)
Fixes [AB#3611725](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3611725) tracks the common-side fix for the MSAL customer issue [#2414](AzureAD/microsoft-authentication-library-for-android#2414). ## Customer-reported issue Resolves the root cause of [microsoft-authentication-library-for-android#2414](AzureAD/microsoft-authentication-library-for-android#2414): when Microsoft Edge is the user''s default browser, MSAL rejects it with `Browser: com.microsoft.emmx signature hash not match` and falls back to WebView. Customers using Edge as the default cannot complete sign-in via custom tabs. ## Root cause Microsoft Edge ships its production APK with **two** signing certificates (APK Signature Scheme v3 lineage). Historically, host apps targeting < API 28 only saw the first signer, so a one-hash safelist entry was sufficient. Once host apps started targeting API 28+, `PackageManager` started returning **all** signers, and `AndroidBrowserSelector.matches()` rejected Edge because it required strict `Set.equals` between the safelist and the browser''s actual signatures. This is a semantic bug in the matcher, not a stale hash -- but the safelist is also one hash short. ## Changes ### 1. `AndroidBrowserSelector.matches()` -- relax signature comparison Replace strict `Set.equals` with `Collections.disjoint()`. A browser is trusted when **any** of its signers appears in the descriptor''s trusted set. This is: - the standard semantic for signature trust (matches AppAuth''s `BrowserSelector` behavior), - safe -- `PackageInfo.signatures` is what Android verified at install time and cannot be forged, - forward-compatible with future Edge / Chrome / other multi-signer or post-rotation browsers. ### 2. `BrowserDescriptor.getBrowserDescriptorForEdge()` -- add rotated hash Add the second Edge signing certificate hash to the switch-browser safelist so the hard-coded Edge descriptor matches both legacy and post-rotation Edge installs even before the matcher change propagates downstream. ### 3. Regression tests - `testSelect_Browser_multiSignerBrowserMatchesWithSubsetSafelist` -- multi-signer browser + single-hash safelist must match (the bug). - `testSelect_Browser_signatureMismatchIsRejected` -- no overlap between presented signatures and safelist must still reject (guards the security boundary). - New `BrowserDescriptorTest` in common4j pins both Edge hashes and the Chrome entry as regression guards. ## Companion PR A companion PR updates `msal_default_config.json` and `auth_config.template.json` in the MSAL repo: AzureAD/microsoft-authentication-library-for-android#2515 (work item 3611726). ## Test plan - New unit tests pass (couldn''t run locally -- Maven feed requires VSTS auth not available in this session; CI will validate). - Existing `AndroidBrowserSelectorTest` cases unchanged in behavior under the new matcher (verified by inspection -- all existing fixtures use identical signature sets between descriptor and browser, so `disjoint == false` corresponds exactly to `equals == true`). ## Risk **Low.** The matcher change is a one-line semantic relaxation that only changes behavior when a browser''s signature set is a proper superset/overlap (not strict equality) with a safelisted descriptor -- exactly the scenario the customer hit. Strict mismatches are still rejected. --------- Co-authored-by: shjameel <shjameel@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c217af2 commit e536e1e

5 files changed

Lines changed: 167 additions & 1 deletion

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
22
----------
3+
- [PATCH] Fix Edge browser selection on devices where Microsoft Edge is the default browser: add the rotated Edge signing certificate hash to the Edge BrowserDescriptor and accept multi-signer browsers when any signature intersects the safelist, instead of requiring strict set-equality (resolves MSAL #2414)
34
- [MINOR] Refactor Auth Tab integration to use provider-based strategy selection. Adds AuthTabStrategyProvider and BrowserLaunchStrategy with Custom Tabs fallback. Compatible with androidx.browser:browser:1.7.0.
45
- [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086)
56
- [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114)

common/src/main/java/com/microsoft/identity/common/internal/ui/browser/AndroidBrowserSelector.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import com.microsoft.identity.common.internal.broker.PackageHelper;
4444

4545
import java.util.ArrayList;
46+
import java.util.Collections;
4647
import java.util.Iterator;
4748
import java.util.List;
4849

@@ -100,7 +101,12 @@ private static boolean matches(@NonNull final BrowserDescriptor descriptor,
100101
return false;
101102
}
102103

103-
if (!descriptor.getSignatureHashes().equals(browser.getSignatureHashes())) {
104+
// An installed browser may present multiple signatures (e.g. APK Signature Scheme v3
105+
// lineage). Trust the browser as long as at least one of its signatures appears in the
106+
// descriptor's trusted set. Strict set-equality previously rejected multi-signer apps
107+
// such as Microsoft Edge once the host app started targeting API 28+ and PackageManager
108+
// began returning all signers instead of just the first one.
109+
if (Collections.disjoint(descriptor.getSignatureHashes(), browser.getSignatureHashes())) {
104110
Logger.warn(methodTag,LOGGING_MSG_BROWSER + browser.getPackageName() + " signature hash not match");
105111
return false;
106112
}

common/src/test/java/com/microsoft/identity/common/internal/ui/browser/AndroidBrowserSelectorTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454

5555
import java.nio.charset.Charset;
5656
import java.util.ArrayList;
57+
import java.util.HashSet;
5758
import java.util.List;
5859
import java.util.Set;
5960

@@ -97,6 +98,20 @@ public class AndroidBrowserSelectorTest {
9798
.setIsCustomTabsSupported(true)
9899
.build();
99100

101+
/**
102+
* Simulates a multi-signer browser (e.g. Microsoft Edge), which presents two distinct
103+
* signatures via APK Signature Scheme v3. Both must be visible to PackageManager when the
104+
* host app targets API 28+.
105+
*/
106+
private static final TestBrowser EDGE_MULTI_SIGNER =
107+
new TestBrowserBuilder("com.microsoft.emmx")
108+
.withBrowserDefaults()
109+
.setVersion("142")
110+
.addSignature("EdgeOriginalSignature")
111+
.addSignature("EdgeRotatedSignature")
112+
.setIsCustomTabsSupported(true)
113+
.build();
114+
100115

101116
//Currently package manager call returns an empty list... failing this test. Needs investigation.
102117
//Ignored while updating to latest Mockito version
@@ -248,6 +263,55 @@ public void testSelect_Browser_preferredBrowserSelected_preferredBrowserNotInSaf
248263
Assert.assertEquals(preferredBrowser.getSignatureHashes(), browser.getSignatureHashes());
249264
}
250265

266+
/**
267+
* Regression test for the customer-reported issue where Microsoft Edge as the default browser
268+
* caused MSAL to fall back to WebView. Edge ships as a multi-signer APK; when the host app
269+
* targets API 28+, PackageManager returns all signers. The safelist contains only one of
270+
* those signers, so strict set-equality used to reject the browser. The match must succeed
271+
* as long as the descriptor's trusted set intersects the browser's actual signatures.
272+
*/
273+
@Test
274+
public void testSelect_Browser_multiSignerBrowserMatchesWithSubsetSafelist() {
275+
setBrowserList(EDGE_MULTI_SIGNER);
276+
277+
// Safelist only carries the original (pre-rotation) Edge signature.
278+
final Set<String> safelistHashes = new HashSet<>();
279+
safelistHashes.add(PackageHelper.generateSignatureHashes(EDGE_MULTI_SIGNER.mPackageInfo).iterator().next());
280+
281+
final List<BrowserDescriptor> browserSafelist = new ArrayList<>();
282+
browserSafelist.add(new BrowserDescriptor(
283+
EDGE_MULTI_SIGNER.mPackageName,
284+
safelistHashes,
285+
null,
286+
null));
287+
288+
final Browser browser = new AndroidBrowserSelector(ApplicationProvider.getApplicationContext()).selectBrowser(browserSafelist, null);
289+
assertNotNull(browser);
290+
assertEquals(EDGE_MULTI_SIGNER.mPackageName, browser.getPackageName());
291+
}
292+
293+
/**
294+
* A browser whose signatures share no element with any safelisted descriptor must still be
295+
* rejected. Guards against accidentally loosening trust beyond "intersection non-empty".
296+
*/
297+
@Test
298+
public void testSelect_Browser_signatureMismatchIsRejected() {
299+
setBrowserList(EDGE_MULTI_SIGNER);
300+
301+
final Set<String> unrelatedHashes = new HashSet<>();
302+
unrelatedHashes.add("not-a-real-edge-signature-hash");
303+
304+
final List<BrowserDescriptor> browserSafelist = new ArrayList<>();
305+
browserSafelist.add(new BrowserDescriptor(
306+
EDGE_MULTI_SIGNER.mPackageName,
307+
unrelatedHashes,
308+
null,
309+
null));
310+
311+
final Browser browser = new AndroidBrowserSelector(ApplicationProvider.getApplicationContext()).selectBrowser(browserSafelist, null);
312+
assertNull(browser);
313+
}
314+
251315
/**
252316
* Browsers are expected to be in priority order, such that the default would be first.
253317
*/

common4j/src/main/com/microsoft/identity/common/java/ui/BrowserDescriptor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ public BrowserDescriptor(
7878
static private BrowserDescriptor getBrowserDescriptorForEdge() {
7979
final HashSet<String> edgeSignatureHashes = new HashSet<>();
8080
edgeSignatureHashes.add("Ivy-Rk6ztai_IudfbyUrSHugzRqAtHWslFvHT0PTvLMsEKLUIgv7ZZbVxygWy_M5mOPpfjZrd3vOx3t-cA6fVQ==");
81+
// Edge production APK is signed with two certificates (APK Signature Scheme v3 lineage).
82+
// AndroidBrowserSelector#matches trusts a browser when at least one of its presented
83+
// signers appears in this set, so listing both covers Edge installs at any point in the
84+
// rotation lifecycle (devices may report one or both signers depending on Edge version
85+
// and host app targetSdk).
86+
edgeSignatureHashes.add("KxJRZ8RFW-6BQa-e4xNE7UmeGU6BWIR_6dzgaAOQWh0rWVENxsXU5TjnWuTR9GqOFbCKMilXKIu7as6VJRjuSw==");
8187
return new BrowserDescriptor(
8288
"com.microsoft.emmx",
8389
edgeSignatureHashes,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.java.ui;
24+
25+
import org.junit.Assert;
26+
import org.junit.Test;
27+
28+
import java.util.List;
29+
30+
/**
31+
* Unit tests for {@link BrowserDescriptor}, verifying the hard-coded switch-browser safelist
32+
* contains the expected packages and signature hashes. These tests also serve as a regression
33+
* guard for the Microsoft Edge multi-signer fix (see MSAL #2414): both Edge signing certificate
34+
* hashes must remain in the descriptor so Edge installs are trusted across the APK signing
35+
* certificate rotation lifecycle.
36+
*/
37+
public class BrowserDescriptorTest {
38+
39+
private static final String EDGE_PACKAGE_NAME = "com.microsoft.emmx";
40+
private static final String EDGE_ORIGINAL_SIGNATURE_HASH =
41+
"Ivy-Rk6ztai_IudfbyUrSHugzRqAtHWslFvHT0PTvLMsEKLUIgv7ZZbVxygWy_M5mOPpfjZrd3vOx3t-cA6fVQ==";
42+
private static final String EDGE_ROTATED_SIGNATURE_HASH =
43+
"KxJRZ8RFW-6BQa-e4xNE7UmeGU6BWIR_6dzgaAOQWh0rWVENxsXU5TjnWuTR9GqOFbCKMilXKIu7as6VJRjuSw==";
44+
private static final String CHROME_PACKAGE_NAME = "com.android.chrome";
45+
46+
@Test
47+
public void switchBrowserSafeListContainsEdgeWithBothSignatureHashes() {
48+
final List<BrowserDescriptor> safeList = BrowserDescriptor.getBrowserSafeListForSwitchBrowser();
49+
50+
BrowserDescriptor edge = null;
51+
for (final BrowserDescriptor descriptor : safeList) {
52+
if (EDGE_PACKAGE_NAME.equals(descriptor.getPackageName())) {
53+
edge = descriptor;
54+
break;
55+
}
56+
}
57+
58+
Assert.assertNotNull("Edge entry must be present in switch-browser safelist", edge);
59+
Assert.assertTrue(
60+
"Original Edge signing certificate hash must remain in the safelist",
61+
edge.getSignatureHashes().contains(EDGE_ORIGINAL_SIGNATURE_HASH));
62+
Assert.assertTrue(
63+
"Rotated Edge signing certificate hash must be present in the safelist (regression for MSAL #2414)",
64+
edge.getSignatureHashes().contains(EDGE_ROTATED_SIGNATURE_HASH));
65+
}
66+
67+
@Test
68+
public void switchBrowserSafeListContainsChrome() {
69+
final List<BrowserDescriptor> safeList = BrowserDescriptor.getBrowserSafeListForSwitchBrowser();
70+
71+
boolean chromeFound = false;
72+
for (final BrowserDescriptor descriptor : safeList) {
73+
if (CHROME_PACKAGE_NAME.equals(descriptor.getPackageName())) {
74+
chromeFound = true;
75+
break;
76+
}
77+
}
78+
79+
Assert.assertTrue("Chrome entry must be present in switch-browser safelist", chromeFound);
80+
}
81+
82+
@Test
83+
public void brokerSafeListContainsChromeOnly() {
84+
final List<BrowserDescriptor> safeList = BrowserDescriptor.getBrowserSafeListForBroker();
85+
86+
Assert.assertEquals("Broker safelist is expected to contain a single entry", 1, safeList.size());
87+
Assert.assertEquals(CHROME_PACKAGE_NAME, safeList.get(0).getPackageName());
88+
}
89+
}

0 commit comments

Comments
 (0)