commit 32e461b30288b12af237a5be9baaa1dda79ded80
parent f481cf7d57a6a68d46330a2bc96e274ba46b2ab2
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 02:03:09 +0000
release: add fail-closed evidence gates
- enforce Rust and JVM advisory and license policy at release readiness
- verify Developer ID signatures and stapled notarization evidence
- keep release credentials outside source and configuration caches
- expose honest audit, license, and release checks through Make
Diffstat:
5 files changed, 174 insertions(+), 2 deletions(-)
diff --git a/Makefile b/Makefile
@@ -4,10 +4,10 @@ GRADLE ?= ./gradlew
CARGO ?= cargo
CARGO_MANIFEST := core/Cargo.toml
-.PHONY: help doctor format format-fix lint test check build bindings dev run package clean
+.PHONY: help doctor format format-fix lint test check build bindings dev run audit licenses package release-check clean
help:
- @printf '%s\n' doctor format format-fix lint test check build bindings dev run package clean
+ @printf '%s\n' doctor format format-fix lint test check build bindings dev run audit licenses package release-check clean
doctor:
java -version
@@ -46,9 +46,23 @@ dev: doctor
run: doctor
$(GRADLE) :app:desktop:run
+audit: doctor
+ $(CARGO) audit --file core/Cargo.lock
+ $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories
+ $(GRADLE) --no-daemon --no-configuration-cache :app:desktop:dependencyCheckAnalyze
+
+licenses: doctor
+ $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml licenses sources
+ $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:checkLicense
+
package: check
$(GRADLE) --no-daemon :app:desktop:verifyMacOsPackage
+release-check: doctor
+ $(CARGO) audit --file core/Cargo.lock
+ $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories licenses sources
+ $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:releaseReadiness
+
clean: doctor
$(CARGO) clean --manifest-path $(CARGO_MANIFEST)
$(GRADLE) --no-daemon clean
diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.github.jk1.license.filter.SpdxLicenseBundleNormalizer
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
@@ -27,6 +28,30 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
+ alias(libs.plugins.license.report)
+ alias(libs.plugins.owasp.dependency.check)
+}
+
+licenseReport {
+ projects = arrayOf(project)
+ configurations = arrayOf("runtimeClasspath")
+ filters = arrayOf(SpdxLicenseBundleNormalizer())
+ allowedLicensesFile = rootProject.layout.projectDirectory.file("config/licenses/allowed-licenses.json")
+}
+
+dependencyCheck {
+ failBuildOnCVSS = 0.0F
+ failOnError = true
+ formats = listOf("HTML", "JSON")
+ scanConfigurations = listOf("runtimeClasspath")
+ skipTestGroups = true
+ providers.environmentVariable("NVD_API_KEY").orNull?.takeIf(String::isNotBlank)?.let {
+ nvd.apiKey = it
+ }
+}
+
+tasks.matching { it.name.startsWith("dependencyCheck") }.configureEach {
+ notCompatibleWithConfigurationCache("Advisory data and environment-only credentials must not be cached")
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
@@ -394,6 +419,82 @@ abstract class VerifyMacOsPackage : DefaultTask() {
}
}
+abstract class VerifyMacOsDeveloperIdSignature : DefaultTask() {
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val appDirectory: DirectoryProperty
+
+ @TaskAction
+ fun verify() {
+ val app = appDirectory.get().asFile
+ commandOutput("/usr/bin/codesign", "--verify", "--deep", "--strict", "--verbose=2", app.absolutePath)
+ val signature = commandOutput("/usr/bin/codesign", "--display", "--verbose=4", app.absolutePath)
+ require(!signature.contains("Signature=adhoc")) {
+ "Release application is ad-hoc signed; a Developer ID Application signature is required"
+ }
+ require(signature.lineSequence().any { it.startsWith("Authority=Developer ID Application:") }) {
+ "Release application is not signed by a Developer ID Application identity"
+ }
+ require(
+ signature.lineSequence().any {
+ it.startsWith("TeamIdentifier=") && it != "TeamIdentifier=not set"
+ },
+ ) {
+ "Release application signature has no Apple team identifier"
+ }
+ }
+
+ private fun commandOutput(vararg command: String): String {
+ val process =
+ ProcessBuilder(*command)
+ .redirectErrorStream(true)
+ .start()
+ val output =
+ process.inputStream
+ .bufferedReader()
+ .use { it.readText() }
+ .trim()
+ require(process.waitFor() == 0) { "Code-signature verification failed: $output" }
+ return output
+ }
+}
+
+abstract class VerifyMacOsNotarization : DefaultTask() {
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val diskImage: RegularFileProperty
+
+ @TaskAction
+ fun verify() {
+ val image = diskImage.get().asFile
+ commandOutput("/usr/bin/xcrun", "stapler", "validate", image.absolutePath)
+ commandOutput(
+ "/usr/sbin/spctl",
+ "--assess",
+ "--type",
+ "open",
+ "--context",
+ "context:primary-signature",
+ "--verbose=2",
+ image.absolutePath,
+ )
+ }
+
+ private fun commandOutput(vararg command: String): String {
+ val process =
+ ProcessBuilder(*command)
+ .redirectErrorStream(true)
+ .start()
+ val output =
+ process.inputStream
+ .bufferedReader()
+ .use { it.readText() }
+ .trim()
+ require(process.waitFor() == 0) { "Notarization verification failed: $output" }
+ return output
+ }
+}
+
dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.compose.foundation)
@@ -530,3 +631,19 @@ val verifyMacOsPackage by tasks.registering(VerifyMacOsPackage::class) {
packageDirectory.set(layout.buildDirectory.dir("compose/binaries/main/dmg"))
expectedFileName.set("$applicationName-$installableVersion.dmg")
}
+val verifyMacOsDeveloperIdSignature by tasks.registering(VerifyMacOsDeveloperIdSignature::class) {
+ dependsOn(verifyMacOsPackage)
+ appDirectory.set(layout.buildDirectory.dir("compose/binaries/main/app/$applicationName.app"))
+}
+val verifyMacOsNotarization by tasks.registering(VerifyMacOsNotarization::class) {
+ dependsOn(verifyMacOsPackage)
+ diskImage.set(layout.buildDirectory.file("compose/binaries/main/dmg/$applicationName-$installableVersion.dmg"))
+}
+tasks.register("releaseReadiness") {
+ dependsOn(
+ "checkLicense",
+ "dependencyCheckAnalyze",
+ verifyMacOsDeveloperIdSignature,
+ verifyMacOsNotarization,
+ )
+}
diff --git a/config/licenses/allowed-licenses.json b/config/licenses/allowed-licenses.json
@@ -0,0 +1,8 @@
+{
+ "allowedLicenses": [
+ { "moduleLicense": "Apache-2.0" },
+ { "moduleLicense": "BSD-2-Clause" },
+ { "moduleLicense": "BSD-3-Clause" },
+ { "moduleLicense": "MIT" }
+ ]
+}
diff --git a/core/deny.toml b/core/deny.toml
@@ -0,0 +1,29 @@
+[advisories]
+ignore = []
+unmaintained = "workspace"
+
+[licenses]
+allow = [
+ "0BSD",
+ "Apache-2.0",
+ "Apache-2.0 WITH LLVM-exception",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "CC0-1.0",
+ "CDLA-Permissive-2.0",
+ "GPL-3.0-only",
+ "ISC",
+ "LGPL-2.1-or-later",
+ "MIT",
+ "MPL-2.0",
+ "Unicode-3.0",
+ "Unlicense",
+ "Zlib",
+]
+confidence-threshold = 0.93
+
+[sources]
+unknown-registry = "deny"
+unknown-git = "deny"
+allow-registry = ["https://github.com/rust-lang/crates.io-index"]
+allow-git = ["https://github.com/rust-nostr/nostr.git"]
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
@@ -5,6 +5,8 @@ jna = "5.17.0"
coroutines = "1.9.0"
detekt = "2.0.0-alpha.5"
ktlint = "14.2.0"
+license-report = "3.1.4"
+owasp-dependency-check = "12.2.2"
[libraries]
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" }
@@ -19,3 +21,5 @@ compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose"
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
detekt = { id = "dev.detekt", version.ref = "detekt" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
+license-report = { id = "com.github.jk1.dependency-license-report", version.ref = "license-report" }
+owasp-dependency-check = { id = "org.owasp.dependencycheck", version.ref = "owasp-dependency-check" }