Skip to content
Open
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
23 changes: 20 additions & 3 deletions app/Sources/NotProtonApp/Model/CrossOverLicense.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ enum CrossOverLicense {

private static let licenseBase = "com.codeweavers.CrossOver"

// SHA-256 of the key CrossOver verifies its own license with (share/crossover/data/tie.pub),
// the same in 26.3 and the 2026 Previews. The bundle's copy is trusted only if it is this
// one: a key swapped into a copied bundle would otherwise verify a license signed with it.
static let codeWeaversKeys: Set<String> = [
"de88c936ee0fe6233314168a4651833abef7ef374ff798fa1c0b548a525a3686",
]

static let notActivatedTitle = String(localized: "CrossOver does not appear to be activated.")
static let trialUnsupported = String(localized: "The trial version is not supported.")
static let notActivatedAdvice = "\(String(localized: "Please run CrossOver and try again.")) \(trialUnsupported)"
Expand All @@ -34,10 +41,11 @@ enum CrossOverLicense {
static func check(
crossOverRoot: URL,
searchDirs: [URL] = defaultSearchDirs,
openssl: String = defaultOpenssl
openssl: String = defaultOpenssl,
keys: Set<String> = codeWeaversKeys
) -> Status {
let status = evaluate(
crossOverRoot: crossOverRoot, searchDirs: searchDirs, openssl: openssl
crossOverRoot: crossOverRoot, searchDirs: searchDirs, openssl: openssl, keys: keys
)
AppLog.note("license: \(status.diagnostic)")
return status
Expand All @@ -46,7 +54,8 @@ enum CrossOverLicense {
private static func evaluate(
crossOverRoot: URL,
searchDirs: [URL],
openssl: String
openssl: String,
keys: Set<String>
) -> Status {
let keyFile = crossOverRoot.appending(
path: "share/crossover/data/tie.pub"
Expand All @@ -60,6 +69,14 @@ enum CrossOverLicense {
)
}

guard let key = Digest.sha256IfPresent(URL(filePath: keyFile)), keys.contains(key) else {
return Status(
licensed: false,
detail: notActivated,
diagnostic: "the verification key in the CrossOver bundle is not CodeWeavers'"
)
}

var rejection: String?

for dir in searchDirs {
Expand Down
38 changes: 28 additions & 10 deletions app/Tests/NotProtonAppTests/CrossOverLicenseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct CrossOverLicenseTests {
var root: URL { SupportPaths.crossOverRoot(inBundle: bundle) }
var publishedKey: URL { root.appending(path: "share/crossover/data/tie.pub") }
var searchDirs: [URL] { [local, system] }
// The fixture's own key stands in for CodeWeavers', read when asked so a test that
// rewrites the key is checked against what is there.
var keys: Set<String> { Set([Digest.sha256IfPresent(publishedKey)].compactMap { $0 }) }

func remove() { try? FileManager.default.removeItem(at: dir) }
}
Expand Down Expand Up @@ -101,18 +104,32 @@ struct CrossOverLicenseTests {
let license = try Self.writeLicense(in: fix.local)
try Self.sign(license, with: fix.key, digest: "-sha256", sidecar: "sha256")

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(status.licensed)
}

// A copied bundle can carry any key, and a license signed with it would verify.
@Test("A license signed with a key that is not CodeWeavers' is refused")
func swappedKeyIsRefused() throws {
let fix = try Self.fixture()
defer { fix.remove() }
let license = try Self.writeLicense(in: fix.local)
try Self.sign(license, with: fix.key, digest: "-sha256", sidecar: "sha256")

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
#expect(!status.licensed)
#expect(status.diagnostic == "the verification key in the CrossOver bundle is not CodeWeavers'")
#expect(status.detail == CrossOverLicense.notActivated)
}

@Test("A bundle carrying no verification key is named in the diagnostic")
func missingKeyIsNamed() throws {
let fix = try Self.fixture(withKey: false)
defer { fix.remove() }
let license = try Self.writeLicense(in: fix.local)
try Self.sign(license, with: fix.key, digest: "-sha256", sidecar: "sha256")

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(!status.licensed)
#expect(status.diagnostic == "no verification key in the CrossOver bundle")
#expect(status.detail == CrossOverLicense.notActivated)
Expand All @@ -125,7 +142,7 @@ struct CrossOverLicenseTests {
let fix = try Self.fixture()
defer { fix.remove() }

let absent = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let absent = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(!absent.licensed)
#expect(absent.diagnostic == "no CrossOver license file found")
#expect(absent.detail == CrossOverLicense.notActivated)
Expand All @@ -135,7 +152,7 @@ struct CrossOverLicenseTests {
try Self.generate(key: other, publishing: nil)
try Self.sign(license, with: other, digest: "-sha256", sidecar: "sha256")

let wrong = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let wrong = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(!wrong.licensed)
#expect(wrong.diagnostic.contains("verified against this bundle"))
#expect(wrong.diagnostic != absent.diagnostic)
Expand All @@ -148,7 +165,7 @@ struct CrossOverLicenseTests {
defer { fix.remove() }
try Self.writeLicense(in: fix.local)

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(!status.licensed)
#expect(status.diagnostic.contains("has no signature beside it"))
#expect(status.detail == CrossOverLicense.notActivated)
Expand All @@ -166,7 +183,7 @@ struct CrossOverLicenseTests {
to: fix.local.appending(path: "\(Self.base).sha256")
)

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(status.licensed)
}

Expand All @@ -177,7 +194,7 @@ struct CrossOverLicenseTests {
let license = try Self.writeLicense(in: fix.system)
try Self.sign(license, with: fix.key, digest: "-sha256", sidecar: "sha256")

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(status.licensed)
#expect(status.diagnostic.contains(fix.system.path(percentEncoded: false)))
}
Expand All @@ -194,7 +211,8 @@ struct CrossOverLicenseTests {
let status = CrossOverLicense.check(
crossOverRoot: fix.root,
searchDirs: fix.searchDirs,
openssl: "/usr/bin/notproton-no-such-openssl"
openssl: "/usr/bin/notproton-no-such-openssl",
keys: fix.keys
)
#expect(!status.licensed)
#expect(status.diagnostic.contains("could not run"))
Expand Down Expand Up @@ -235,7 +253,7 @@ struct CrossOverLicenseTests {
try Self.sign(license, with: fix.key, digest: "-sha256", sidecar: "sha256")
try Data("not a key".utf8).write(to: fix.publishedKey)

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(!status.licensed)
#expect(status.diagnostic == "the verification key in the CrossOver bundle could not be read")
#expect(status.detail == CrossOverLicense.notActivated)
Expand All @@ -254,7 +272,7 @@ struct CrossOverLicenseTests {
.path(percentEncoded: false)
)

let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs)
let status = CrossOverLicense.check(crossOverRoot: fix.root, searchDirs: fix.searchDirs, keys: fix.keys)
#expect(!status.licensed)
#expect(status.diagnostic.contains("could not be checked"))
#expect(status.detail == CrossOverLicense.notActivated)
Expand Down
31 changes: 31 additions & 0 deletions dylib/feats/compat_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,37 @@ if [ -z "$np_build" ] || [ ! -d "$CX_ROOT/lib/wine" ]; then
fi
echo "runner: build $np_build ($np_display) at $CX_ROOT" >> "$log" 2>&1 || true

# CrossOver does not check its license when its Wine is run directly, so a runner copied to a
# Mac without a licensed CrossOver would start games too. This is the check the app makes before
# it sets a runner up, made again at each launch: the license in the preferences, verified with
# the runner's key, which is only trusted if it is CodeWeavers' (by SHA-256, as the app does).
np_license_keys="de88c936ee0fe6233314168a4651833abef7ef374ff798fa1c0b548a525a3686"
crossover_licensed() {
key="$CX_ROOT/share/crossover/data/tie.pub"
digest=$(/usr/bin/shasum -a 256 "$key" 2>/dev/null) || return 1
case " $np_license_keys " in
*" ${digest%% *} "*) ;;
*) return 1 ;;
esac
for dir in "$HOME/Library/Preferences" /Library/Preferences; do
license="$dir/com.codeweavers.CrossOver.license"
[ -f "$license" ] || continue
for pair in sha256:-sha256 sig:-sha1; do
signature="$dir/com.codeweavers.CrossOver.${pair%%:*}"
[ -f "$signature" ] || continue
/usr/bin/openssl dgst "${pair#*:}" -verify "$key" -signature "$signature" "$license" \
>/dev/null 2>&1 && return 0
done
done
return 1
}
stage_step="license check"
if ! crossover_licensed; then
echo "=== no CrossOver license verifies with CodeWeavers' key, run CrossOver to activate it ===" >> "$log" 2>&1 || true
show_alert "CrossOver does not appear to be activated." "Please run CrossOver and try again. The trial version is not supported."
exit 1
fi

# Each CrossOver build needs its own template.
# FEX builds need two, one for FEX/arm64 Wine and one for Rosetta/AMD64 Wine
runner_id=""
Expand Down
31 changes: 31 additions & 0 deletions dylib/tests/buildcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ launch() {
[ ! -f "$data/notproton-build" ] || head -1 "$data/notproton-build"
}

echo "== CrossOver license =="
license_fn=$(extract crossover_licensed)
[ -n "$license_fn" ] || { echo "FAIL: crossover_licensed not found in $SRC"; exit 1; }
lic="$work/license"
prefs="$lic/home/Library/Preferences"
mkdir -p "$prefs" "$lic/cx/share/crossover/data"
openssl genrsa -out "$lic/private.pem" 2048 2>/dev/null
openssl rsa -in "$lic/private.pem" -pubout -out "$lic/cx/share/crossover/data/tie.pub" 2>/dev/null
printf '[crossmac]\ncustomer=Test\n' > "$prefs/com.codeweavers.CrossOver.license"
ours=$(shasum -a 256 "$lic/cx/share/crossover/data/tie.pub" | cut -d' ' -f1)
licensed() {
(
HOME="$lic/home"
CX_ROOT="$lic/cx"
np_license_keys=$1
eval "$license_fn"
crossover_licensed
) && echo yes || echo no
}
is "a license with no signature beside it is refused" no "$(licensed "$ours")"
openssl dgst -sha256 -sign "$lic/private.pem" -out "$prefs/com.codeweavers.CrossOver.sha256" \
"$prefs/com.codeweavers.CrossOver.license"
is "a license the accepted key verifies passes" yes "$(licensed "$ours")"
is "the same license is refused when the key is not one CodeWeavers ships" no "$(licensed 0000)"
printf 'customer=Someone else\n' >> "$prefs/com.codeweavers.CrossOver.license"
is "an edited license is refused" no "$(licensed "$ours")"
app_keys=$(sed -n 's/^ *"\([0-9a-f]\{64\}\)",$/\1/p' \
"$(dirname "$SRC")/../../app/Sources/NotProtonApp/Model/CrossOverLicense.swift")
is "the run script and the app trust the same key" "$app_keys" \
"$(sed -n 's/^np_license_keys="\(.*\)"$/\1/p' "$SRC")"

echo "== prefixes from before the record =="
is "a fresh prefix is claimed" "status=0
26.3.0.39832" "$(launch 26.3.0.39832 "" "")"
Expand Down