Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,12 @@ private static void collectFileProblems(List<Problem> problems, Properties setti
return;
}

Problem wrongApp = checkProfileSignsThisApp(profile, settings, describe, settingKey);
if (wrongApp != null) {
problems.add(wrongApp);
return;
}

if (!checkMethodMismatch) {
return;
}
Expand All @@ -1072,6 +1078,55 @@ private static void collectFileProblems(List<Problem> problems, Properties setti
}
}

/**
* Whether the profile's App ID covers the bundle identifier this build will stamp on the app.
*
* <p>This is the check that was missing while {@link #profileCoversBundleId} was already
* being applied to every app EXTENSION: the app's own profile was only ever checked for
* readability, expiry and distribution method, so a profile belonging to a different App ID
* passed preflight and failed minutes later on the build server with
* {@code Provisioning profile "..." doesn't match the entitlements file's values for the
* application-identifier and keychain-access-groups entitlements}. Both of those entitlements
* are {@code $(AppIdentifierPrefix)$(CFBundleIdentifier)}, which is to say both of them are
* this comparison, spelled by Xcode after the upload (issues #5773 and #5793).
*
* <p>Fatal, because there is nothing ambiguous left by the time it fires: a wildcard App ID
* is matched by {@link #profileCoversBundleId}, an unresolved or absent package name is not
* judged at all, and a profile that names no App ID has already returned above. What remains
* is a profile Apple issued for a different application.
*
* <p>What it does NOT catch is the same bundle identifier under a different TEAM prefix, and
* that half of the Xcode message stays a build-server failure. {@code profileCoversBundleId}
* compares the App ID pattern with the prefix stripped, deliberately: nothing in
* {@code codenameone_settings.properties} states the team, so the only honest comparison here
* is the one that does not need it.
*
* @return the problem, or null when the profile covers this app or nothing here can tell
*/
private static Problem checkProfileSignsThisApp(Profile profile, Properties settings,
String describe, String settingKey) {
String bundleId = trimmed(settings.getProperty("codename1.packageName"));
if (bundleId == null || bundleId.isEmpty() || bundleId.indexOf("${") >= 0) {
// The same rule the rest of this class follows: what it cannot resolve, it may not
// judge. A build with no package name fails elsewhere, and with a better message.
return null;
}
if (profile.applicationIdentifier == null || profile.applicationIdentifier.isEmpty()) {
return null;
}
if (profileCoversBundleId(profile.applicationIdentifier, bundleId)) {
return null;
}
return new Problem("The provisioning profile " + describe + " is for App ID "
+ appIdPattern(profile.applicationIdentifier) + ", which cannot sign \"" + bundleId
+ "\" (codename1.packageName). Signing fails on the application-identifier and "
+ "keychain-access-groups entitlements, because both of them are the App ID.\n"
+ "Either point " + settingKey + " at a profile issued for " + bundleId
+ ", or set codename1.packageName to the bundle ID the profile was issued for. "
+ "The certificate wizard creates a matching profile from the project's own "
+ "package name.", true);
}

/**
* The type mismatch {@code xcodebuild -exportArchive} would reject minutes into the
* cloud build. Enterprise profiles only warn: Xcode accepts an in-house profile for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ private static String development(String name) {
+ "<key>Entitlements</key><dict><key>get-task-allow</key><true/></dict>\n");
}

/** An App Store profile that names the App ID it was issued for, the way Apple's do. */
private static String appStoreFor(String name, String applicationIdentifier) {
return profile(name, FUTURE,
"<key>Entitlements</key><dict>"
+ "<key>get-task-allow</key><false/>"
+ "<key>application-identifier</key><string>" + applicationIdentifier + "</string>"
+ "</dict>\n");
}

private static String enterprise(String name) {
return profile(name, FUTURE,
"<key>ProvisionsAllDevices</key><true/>\n"
Expand Down Expand Up @@ -186,6 +195,71 @@ public void expiredProfileIsRefused() throws Exception {
assertFatal(check(settings(f, true, null), true), "Old Profile", "expired on");
}

// ---- the profile is for a different app ----

/**
* Issues #5773 and #5793: an App Store profile for one App ID, configured on a project whose
* package name is another. Both reporters spent a cloud build each to be told
* "Provisioning profile ... doesn't match the entitlements file's values for the
* application-identifier and keychain-access-groups entitlements", which is this, after the
* upload. Every fact needed was on disk before the build was sent.
*/
@Test
public void profileForAnotherAppIsRefused() throws Exception {
Properties p = settings(write(appStoreFor("dtest11 STORE", "ABCD1234.com.example.other")),
true, "app-store");
p.setProperty("codename1.packageName", "com.example.app");
assertFatal(check(p, true), "dtest11 STORE", "com.example.other", "com.example.app",
"application-identifier", "keychain-access-groups");
}

/** The same profile, on the project it was actually issued for. */
@Test
public void profileForThisAppPasses() throws Exception {
Properties p = settings(write(appStoreFor("Store", "ABCD1234.com.example.app")),
true, "app-store");
p.setProperty("codename1.packageName", "com.example.app");
assertTrue(check(p, true).isEmpty());
}

/** A wildcard App ID covers the bundle id, and refusing it would block a working build. */
@Test
public void wildcardProfileIsNotRefused() throws Exception {
Properties p = settings(write(appStoreFor("Wildcard", "ABCD1234.com.example.*")),
true, "app-store");
p.setProperty("codename1.packageName", "com.example.app");
assertTrue(check(p, true).isEmpty());
}

/**
* The team prefix is not part of the comparison, so a profile from another team under the
* same bundle id still passes here. Asserted rather than left implied: it is the half of the
* Xcode message this check cannot answer, because nothing in the settings states the team.
*/
@Test
public void aDifferentTeamPrefixIsNotJudged() throws Exception {
Properties p = settings(write(appStoreFor("Other Team", "ZZZZ9999.com.example.app")),
true, "app-store");
p.setProperty("codename1.packageName", "com.example.app");
assertTrue(check(p, true).isEmpty());
}

/** No package name to compare against, and a profile is not refused on a guess. */
@Test
public void absentPackageNameIsNotJudged() throws Exception {
Properties p = settings(write(appStoreFor("Store", "ABCD1234.com.example.other")),
true, "app-store");
assertTrue(check(p, true).isEmpty());
}

/** A profile that names no App ID at all says nothing either way. */
@Test
public void profileWithoutAnAppIdIsNotJudged() throws Exception {
Properties p = settings(write(appStore("Store")), true, "app-store");
p.setProperty("codename1.packageName", "com.example.app");
assertTrue(check(p, true).isEmpty());
}

// ---- the combinations that must NOT be refused ----

@Test
Expand Down
12 changes: 11 additions & 1 deletion scripts/certificatewizard/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
</parent>
<artifactId>cn1-certificatewizard-common</artifactId>
<packaging>jar</packaging>

<properties>
<cn1.certificatewizard.skipTests>true</cn1.certificatewizard.skipTests>
</properties>
<name>cn1-certificatewizard-common</name>
<description>Shared UI and service code for the Codename One Certificate Wizard.</description>

Expand Down Expand Up @@ -152,7 +156,13 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
<!-- Skipped by default: these tests need a JDK 17 toolchain and the CN1
simulator classes, which the packaging build does not set up. The value
was hardcoded true, which meant -DskipTests=false could not reach it and
the suite could not be run at all, here or anywhere. Opt in with
-Dcn1.certificatewizard.skipTests=false, the same switch
scripts/settings/common uses for the same reason. -->
<skipTests>${cn1.certificatewizard.skipTests}</skipTests>
</configuration>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,12 +449,25 @@ private void overviewPage() {
Container setup = new Container(new GridLayout(1, 3));
setupCard(setup, "ASC API Key", state.credential.configured() ? "Connected" : "Not configured",
state.credential.configured() ? state.credential.keyId() : "-");
// Each card answers for ITS OWN slot. Both used to show row zero of an unfiltered list,
// so an account holding a Mac certificate and a development profile was told "Apple
// distribution certificate: Ready -- <name> MAC APP DISTRIBUTION" and "App Store profile:
// Ready -- <name> Development" (issue #5773). Neither slot was filled, and the reporter
// reasonably concluded the wizard was signing iOS builds with Mac App Store assets. It was
// not -- WizardDecisions.certificateTypeSatisfies has always been strict about this -- but
// a readiness panel that reports readiness it does not have is worse than no panel.
SigningState.Certificate distribution = WizardDecisions.distributionCertificateForOverview(state);
setupCard(setup, "Apple distribution certificate",
state.certificates.isEmpty() ? "None active" : "Ready",
state.certificates.isEmpty() ? "-" : state.certificates.get(0).displayName());
distribution == null ? "None active" : "Ready",
distribution == null ? "-" : distribution.displayName());
// Scoped to this project's bundle id. The certificate card is not, and that asymmetry is
// deliberate: an Apple Distribution certificate signs anything the team ships, while a
// provisioning profile is issued for one App ID and another app's is no use here.
SigningState.Profile appStore = WizardDecisions.appStoreProfileForOverview(
state, projectBundleIdentifier());
setupCard(setup, "App Store profile",
state.profiles.isEmpty() ? "None yet" : "Ready",
state.profiles.isEmpty() ? "-" : state.profiles.get(0).name());
appStore == null ? "None yet" : "Ready",
appStore == null ? "-" : appStore.name());
page.add(setup);
label(page, "YOUR ASSETS", "CWNavLabel");
Container metrics = new Container(new GridLayout(1, 4));
Expand Down Expand Up @@ -1144,13 +1157,46 @@ private void bundleDialog(String initialIdentifier, String initialName, String p
showModal(d);
}

/// Registers a device, on the platform the developer picks.
///
/// The platform used to be hardcoded to iOS, which made the Mac profile types this wizard
/// offers unreachable: a Mac profile may only name MAC_OS devices ([WizardDecisions#isUsableDevice]),
/// an iOS device is correctly hidden from its picker, and no Mac device could be registered
/// here to put in it. The result was a profile type you could select and never create
/// (issue #5773). The service has always accepted either platform -- only this dialog did not
/// ask.
///
/// A Mac is registered by its Provisioning UDID (the hardware UUID), not by the 25-character
/// identifier an iPhone has, so the hint follows the selection.
private void deviceDialog() {
InteractionDialog d = modal("Register device");
final String[] platform = {"IOS"};
label(d, "Platform", "CWFieldLabel");
final String[] platformValues = {"IOS", "MAC_OS"};
// Spelled out rather than derived with toLowerCase(): these are component names, and
// String.toLowerCase() is locale sensitive with no root-locale overload in this runtime,
// so on a Turkish device the "I" of "IOS" folds to a dotless i and the name changes.
final String[] platformNames = {"pick.devicePlatform.ios", "pick.devicePlatform.mac_os"};
final Button[] platformButtons = {segment("iPhone or iPad", true), segment("Mac", false)};
TextField name = field("Device name", "QA iPhone");
TextField udid = field("UDID", "00008120-000A1C3E0C68201E");
for (int i = 0; i < platformButtons.length; i++) {
platformButtons[i].setName(platformNames[i]);
final int index = i;
platformButtons[i].addActionListener(e -> {
platform[0] = platformValues[index];
updateSegmentButtons(platformButtons, platformValues, platform[0]);
boolean mac = "MAC_OS".equals(platform[0]);
name.setHint(mac ? "Build Mac" : "QA iPhone");
udid.setHint(mac ? "Provisioning UDID (hardware UUID)"
: "00008120-000A1C3E0C68201E");
d.revalidate();
});
}
d.add(actionRow(Component.LEFT, platformButtons[0], platformButtons[1]));
d.add(name).add(udid);
Button save = primary("Register", "modal.device.submit");
save.addActionListener(e -> { d.dispose(); service.registerDevice(name.getText(), udid.getText(), r -> afterMutation(r, "Device registered")); });
save.addActionListener(e -> { d.dispose(); service.registerDevice(name.getText(), udid.getText(), platform[0], r -> afterMutation(r, "Device registered")); });
addDialogActions(d, save);
showModal(d);
}
Expand Down Expand Up @@ -2727,7 +2773,12 @@ private void afterCertificateDownload(SigningService.Result<String> r, SigningSt
}
latestCertificatePath = r.value;
latestCertificatePassword = password == null ? "" : password;
latestAssetsDebug = "IOS_DEVELOPMENT".equals(c.certificateType());
// isDevelopmentCertificate, not an equality test against IOS_DEVELOPMENT. Apple's generic
// "Apple Development" type (DEVELOPMENT) supersedes the platform-specific one and every
// other decision in this class already reads it that way; only here did it fall through to
// "release", which offered to install a development certificate into
// codename1.ios.release.certificate and overwrite the unqualified key with it.
latestAssetsDebug = isDevelopmentCertificate(c.certificateType());
offerInstall();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,44 @@ public void createCertificate(String certificateType, String displayName, OnComp
r -> done(r, callback));
}

/// "Sync with Apple": reconcile BOTH certificates and provisioning profiles.
///
/// This used to call the certificate reconcile alone, and there was no profile reconcile to
/// call -- the profile list was served from rows this wizard had itself created, so a sync
/// could not bring it into step with Apple no matter how often it ran. That is issue #5793:
/// a list showing 11 of an account's 24 profiles, a row for a profile deleted in the portal
/// that answers "Apple no longer has this item" straight after a sync, and a create refused
/// for a duplicate name that is invisible here.
///
/// Sequential rather than parallel, and profiles second: the certificate pass is the one
/// whose failure means the key is unusable, and reporting that is more useful than reporting
/// whichever of the two happened to answer first.
public void reconcile(OnComplete<Result<Void>> callback) {
certificatesApi.reconcileCertificates(bearerToken, r -> done(r, callback));
certificatesApi.reconcileCertificates(bearerToken, r -> {
if (!ok(r)) {
callback.completed(Result.fail(error(r)));
return;
}
profilesApi.reconcileProfiles(bearerToken, rr -> {
if (ok(rr) || endpointAbsent(rr)) {
// endpointAbsent: a wizard newer than the signing service it is talking to.
// The two ship separately, so for the window where an older service is
// deployed the certificate half must still work rather than the whole sync
// failing on a route that does not exist yet. Every other status is a real
// failure and is reported.
callback.completed(Result.<Void>ok(null));
return;
}
callback.completed(Result.<Void>fail(error(rr)));
});
});
}

/// Whether this reply means the route is not there at all, as opposed to the request being
/// refused. 404 is what a service without the profile reconcile answers; 405 is what one
/// answers that has the /profiles/{id} routes but not this one.
private static boolean endpointAbsent(Response<?> r) {
return r != null && (r.getResponseCode() == 404 || r.getResponseCode() == 405);
}

public void revokeCertificate(Long id, OnComplete<Result<Void>> callback) {
Expand Down Expand Up @@ -251,8 +287,9 @@ public void enablePushCapability(String bundleIdAppleId, OnComplete<Result<Void>
bearerToken, r -> done(r, callback));
}

public void registerDevice(String name, String udid, OnComplete<Result<Void>> callback) {
devicesApi.registerDevice(new RegisterDeviceRequest(name, udid, "IOS"), bearerToken, r -> done(r, callback));
public void registerDevice(String name, String udid, String platform, OnComplete<Result<Void>> callback) {
String plat = platform == null || platform.trim().isEmpty() ? "IOS" : platform.trim();
devicesApi.registerDevice(new RegisterDeviceRequest(name, udid, plat), bearerToken, r -> done(r, callback));
}

public void createProfile(String name, String profileType, String bundleIdAppleId, List<String> certificateAppleIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,9 @@ public List<String> appGroupAssociation(String bundleIdAppleId) {
return assoc == null ? new ArrayList<String>() : new ArrayList<String>(assoc);
}

public void registerDevice(String name, String udid, OnComplete<Result<Void>> callback) {
devices.add(new SigningState.Device("DEV_" + (++seq), name, udid, "IOS", "ENABLED"));
public void registerDevice(String name, String udid, String platform, OnComplete<Result<Void>> callback) {
String plat = platform == null || platform.trim().isEmpty() ? "IOS" : platform.trim();
devices.add(new SigningState.Device("DEV_" + (++seq), name, udid, plat, "ENABLED"));
callback.completed(Result.ok(null));
}

Expand Down
Loading
Loading