Skip to content

Commit 966abab

Browse files
strogiyotecrozzavbabanin
authored
Upgrade libmongocrypt version to 1.18.1 (#1983)
JAVA-6187 --------- Co-authored-by: Almas Abdrazak <abdrazak.almas@mongodb.com> Co-authored-by: Ross Lawley <ross@mongodb.com> Co-authored-by: slav.babanin <slav.babanin@mongodb.com>
1 parent 98e0761 commit 966abab

1 file changed

Lines changed: 245 additions & 51 deletions

File tree

mongodb-crypt/build.gradle.kts

Lines changed: 245 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
import ProjectExtensions.configureJarManifest
1717
import ProjectExtensions.configureMavenPublication
1818
import de.undercouch.gradle.tasks.download.Download
19+
import java.io.ByteArrayOutputStream
20+
import javax.inject.Inject
21+
import org.gradle.api.GradleException
22+
import org.gradle.process.ExecOperations
1923

2024
plugins {
2125
id("project.java")
@@ -48,72 +52,256 @@ configureJarManifest {
4852
*/
4953
val jnaDownloadsDir = rootProject.file("build/jnaLibs/downloads/").path
5054
val jnaResourcesDir = rootProject.file("build/jnaLibs/resources/").path
55+
56+
tasks.clean { delete(rootProject.file("build/jnaLibs")) }
57+
5158
val jnaLibPlatform: String =
5259
if (com.sun.jna.Platform.RESOURCE_PREFIX.startsWith("darwin")) "darwin" else com.sun.jna.Platform.RESOURCE_PREFIX
53-
val jnaLibsPath: String = System.getProperty("jnaLibsPath", "${jnaResourcesDir}${jnaLibPlatform}")
60+
// When -DjnaLibsPath is set, the user wants to use a pre-existing local copy of the libmongocrypt
61+
// binaries instead of fetching them from the libmongocrypt GitHub release, so we skip the whole
62+
// download / verify / extract chain.
63+
val userSuppliedJnaLibsPath: String? = System.getProperty("jnaLibsPath")
64+
val jnaLibsPath: String = userSuppliedJnaLibsPath ?: "${jnaResourcesDir}/${jnaLibPlatform}"
5465
val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath)
5566

56-
// Download jnaLibs that match the git tag or revision to jnaResourcesBuildDir
57-
val downloadRevision = "1.17.3"
58-
val binariesArchiveName = "libmongocrypt-java.tar.gz"
67+
// Download the libmongocrypt per-platform tarballs (and their signatures) to jnaDownloadsDir.
68+
// To upgrade: change downloadRevision, run `./gradlew clean downloadJnaLibs`, and verify the build.
69+
val downloadRevision = "1.18.1"
70+
val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/download/$downloadRevision"
5971

6072
/**
61-
* The name of the archive includes downloadRevision to ensure that:
62-
* - the archive is downloaded if the revision changes.
63-
* - the archive is not downloaded if the revision is the same and archive had already been saved in build output.
73+
* Maps a JNA platform key (the directory consumed by `jna.library.path`) to the libmongocrypt GitHub release tarball
74+
* that ships its native library, plus the path of that library inside the tarball. The tarball name and its internal
75+
* layout differ per platform, so both must be tracked explicitly.
76+
*
77+
* libmongocrypt's signature assets replace the `.tar.gz` suffix with `.asc` (e.g.
78+
* `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-1.18.1.asc`).
6479
*/
65-
val localBinariesArchiveName = "libmongocrypt-java-$downloadRevision.tar.gz"
66-
67-
val downloadUrl: String =
68-
"https://mciuploads.s3.amazonaws.com/libmongocrypt/java/$downloadRevision/$binariesArchiveName"
80+
data class CryptBinary(val jnaPlatform: String, val tarball: String, val libPathInTarball: String) {
81+
val signature: String = tarball.removeSuffix(".tar.gz") + ".asc"
82+
}
6983

70-
val jnaMapping: Map<String, String> =
71-
mapOf(
72-
"rhel-62-64-bit" to "linux-x86-64",
73-
"rhel72-zseries-test" to "linux-s390x",
74-
"rhel-71-ppc64el" to "linux-ppc64le",
75-
"ubuntu1604-arm64" to "linux-aarch64",
76-
"windows-test" to "win32-x86-64",
77-
"macos" to "darwin")
84+
val cryptBinaries: List<CryptBinary> =
85+
listOf(
86+
CryptBinary(
87+
"linux-x86-64",
88+
"libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-$downloadRevision.tar.gz",
89+
"lib64/libmongocrypt.so"),
90+
CryptBinary(
91+
"linux-s390x",
92+
"libmongocrypt-linux-s390x-glibc_2_7-nocrypto-$downloadRevision.tar.gz",
93+
"lib64/libmongocrypt.so"),
94+
CryptBinary(
95+
"linux-ppc64le",
96+
"libmongocrypt-linux-ppc64le-glibc_2_17-nocrypto-$downloadRevision.tar.gz",
97+
"lib64/libmongocrypt.so"),
98+
CryptBinary(
99+
"linux-aarch64",
100+
"libmongocrypt-linux-arm64-glibc_2_17-nocrypto-$downloadRevision.tar.gz",
101+
"lib64/libmongocrypt.so"),
102+
CryptBinary("win32-x86-64", "libmongocrypt-windows-x86_64-$downloadRevision.tar.gz", "bin/mongocrypt.dll"),
103+
CryptBinary("darwin", "libmongocrypt-macos-universal-$downloadRevision.tar.gz", "lib/libmongocrypt.dylib"))
78104

79105
sourceSets { main { java { resources { srcDirs(jnaResourcesDir) } } } }
80106

81-
tasks.register<Download>("downloadJava") {
82-
src(downloadUrl)
83-
dest("${jnaDownloadsDir}/$localBinariesArchiveName")
84-
overwrite(true)
85-
/* To make sure we don't download archive with binaries if it hasn't been changed in S3 bucket since last download.*/
86-
onlyIfModified(true)
87-
}
107+
/**
108+
* Public key used to sign libmongocrypt release tarballs. See:
109+
* https://www.mongodb.com/docs/manual/tutorial/verify-mongodb-packages/#std-label-verify-pkgs
110+
*/
111+
val libmongocryptPublicKeyUrl = "https://pgp.mongodb.com/libmongocrypt.pub"
112+
val libmongocryptPublicKeyFile = "libmongocrypt.pub"
113+
114+
tasks.register<Download>("downloadCryptLibs") {
115+
src(
116+
cryptBinaries.flatMap { listOf("$downloadUrlBase/${it.tarball}", "$downloadUrlBase/${it.signature}") } +
117+
libmongocryptPublicKeyUrl)
118+
dest(jnaDownloadsDir)
119+
/* Reuse already-downloaded files. Useful for offline builds and reduces network churn. */
120+
overwrite(false)
121+
quiet(true)
122+
123+
/* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
124+
onlyIf { userSuppliedJnaLibsPath == null }
88125

89-
tasks.register<Copy>("unzipJava") {
90-
/*
91-
Clean up the directory first if the task is not UP-TO-DATE.
92-
This can happen if the download revision has been changed and the archive is downloaded again.
93-
*/
94126
doFirst {
95-
println("Cleaning up $jnaResourcesDir")
96-
delete(jnaResourcesDir)
127+
val missing = cryptBinaries.filter { !file("$jnaDownloadsDir/${it.tarball}").exists() }
128+
if (missing.isNotEmpty()) {
129+
logger.lifecycle("Downloading libmongocrypt $downloadRevision binaries:")
130+
missing.forEach { logger.lifecycle(" ${it.tarball}") }
131+
}
132+
}
133+
}
134+
135+
/*
136+
* Verify the signature of every downloaded libmongocrypt tarball before extracting it.
137+
* Per DRIVERS-3441, drivers that bundle libmongocrypt must verify GPG signatures of
138+
* release tarballs against the official MongoDB libmongocrypt signing key.
139+
*
140+
* The keyring is kept under `build/` so this task does not touch the developer's
141+
* system GPG keyring and so `./gradlew clean` resets the trust state.
142+
*/
143+
val skipCryptVerify = providers.gradleProperty("skipCryptVerify").map { it.toBoolean() }.orElse(false)
144+
145+
abstract class VerifyLibmongocryptTask : DefaultTask() {
146+
@get:Inject abstract val execOps: ExecOperations
147+
148+
@get:InputFiles abstract val tarballs: ConfigurableFileCollection
149+
@get:InputFiles abstract val signatures: ConfigurableFileCollection
150+
@get:InputFile abstract val publicKey: RegularFileProperty
151+
@get:Input abstract val skipVerify: Property<Boolean>
152+
@get:Input abstract val expectedFingerprint: Property<String>
153+
@get:OutputFile abstract val verificationStamp: RegularFileProperty
154+
155+
/* Scratch keyring directory. Marked @Internal (not @OutputDirectory) because the directory is
156+
* genuinely ephemeral - nothing downstream consumes it. */
157+
@get:Internal abstract val gnupgHome: DirectoryProperty
158+
159+
@TaskAction
160+
fun verify() {
161+
if (skipVerify.get()) {
162+
logger.warn(
163+
"SKIPPING libmongocrypt signature verification because -PskipCryptVerify=true was set. " +
164+
"Do not use this for release builds.")
165+
verificationStamp.get().asFile.writeText("Skipped verification at ${System.currentTimeMillis()}")
166+
return
167+
}
168+
169+
try {
170+
execOps.exec {
171+
commandLine("gpg", "--version")
172+
standardOutput = ByteArrayOutputStream()
173+
}
174+
} catch (e: Exception) {
175+
throw GradleException(
176+
"gpg is required to verify libmongocrypt tarballs since 1.18.0 but was not found on PATH. " +
177+
"Install gpg (e.g. `apt-get install gnupg`, `brew install gnupg`, Gpg4win on Windows), " +
178+
"or pass -PskipCryptVerify=true for offline development builds.",
179+
e)
180+
}
181+
182+
val home =
183+
gnupgHome.get().asFile.apply {
184+
deleteRecursively()
185+
mkdirs()
186+
// GPG refuses to use a homedir with permissions broader than the owner.
187+
setReadable(false, false)
188+
setReadable(true, true)
189+
setWritable(false, false)
190+
setWritable(true, true)
191+
setExecutable(false, false)
192+
setExecutable(true, true)
193+
}
194+
195+
execOps.exec {
196+
commandLine(
197+
"gpg",
198+
"--homedir",
199+
home.path,
200+
"--batch",
201+
"--quiet",
202+
"--no-autostart",
203+
"--import",
204+
publicKey.get().asFile.path)
205+
standardOutput = ByteArrayOutputStream()
206+
errorOutput = ByteArrayOutputStream()
207+
}
208+
209+
try {
210+
execOps.exec {
211+
commandLine(
212+
"gpg",
213+
"--homedir",
214+
home.path,
215+
"--batch",
216+
"--no-autostart",
217+
"--with-colons",
218+
"--fingerprint",
219+
expectedFingerprint.get())
220+
standardOutput = ByteArrayOutputStream()
221+
errorOutput = ByteArrayOutputStream()
222+
}
223+
} catch (e: Exception) {
224+
throw GradleException(
225+
"Imported libmongocrypt signing key fingerprint does not match expected value " +
226+
"${expectedFingerprint.get()}. The downloaded public key may have been rotated.",
227+
e)
228+
}
229+
230+
// Pair tarballs with signatures by basename; ConfigurableFileCollection.files is an
231+
// unordered Set, so zipping the two collections could mismatch pairs.
232+
val signaturesByName = signatures.files.associateBy { it.name }
233+
tarballs.files.forEach { tarball ->
234+
val signatureName = tarball.name.removeSuffix(".tar.gz") + ".asc"
235+
val signature =
236+
signaturesByName[signatureName]
237+
?: throw GradleException(
238+
"Missing signature $signatureName for ${tarball.name}; expected it next to the tarball.")
239+
val verifyErr = ByteArrayOutputStream()
240+
try {
241+
execOps.exec {
242+
commandLine(
243+
"gpg",
244+
"--homedir",
245+
home.path,
246+
"--batch",
247+
"--quiet",
248+
"--no-autostart",
249+
"--trust-model",
250+
"always",
251+
"--verify",
252+
signature.path,
253+
tarball.path)
254+
standardOutput = ByteArrayOutputStream()
255+
errorOutput = verifyErr
256+
}
257+
} catch (e: Exception) {
258+
throw GradleException(
259+
"GPG signature verification failed for ${tarball.name}:\n${verifyErr.toString().trim()}", e)
260+
}
261+
}
262+
263+
verificationStamp
264+
.get()
265+
.asFile
266+
.writeText(
267+
"verified=${System.currentTimeMillis()}\n" + "tarballs=${tarballs.files.joinToString { it.name }}\n")
268+
}
269+
}
270+
271+
tasks.register<VerifyLibmongocryptTask>("verifyCryptLibs") {
272+
dependsOn("downloadCryptLibs")
273+
tarballs.from(cryptBinaries.map { "$jnaDownloadsDir/${it.tarball}" })
274+
signatures.from(cryptBinaries.map { "$jnaDownloadsDir/${it.signature}" })
275+
publicKey.set(file("$jnaDownloadsDir/$libmongocryptPublicKeyFile"))
276+
skipVerify.set(skipCryptVerify)
277+
expectedFingerprint.set("F2F5BF4ABF517E039AFCADAA81F1404DEBACA586")
278+
gnupgHome.set(layout.buildDirectory.dir("jnaLibs/gnupg"))
279+
verificationStamp.set(layout.buildDirectory.file("jnaLibs/verified.stamp"))
280+
281+
/* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
282+
onlyIf { userSuppliedJnaLibsPath == null }
283+
}
284+
285+
tasks.register<Sync>("extractCryptLibs") {
286+
cryptBinaries.forEach { spec ->
287+
from(tarTree(resources.gzip("$jnaDownloadsDir/${spec.tarball}"))) {
288+
include(spec.libPathInTarball)
289+
eachFile { path = "${spec.jnaPlatform}/${name}" }
290+
includeEmptyDirs = false
291+
}
97292
}
98-
from(tarTree(resources.gzip("${jnaDownloadsDir}/$localBinariesArchiveName")))
99-
include(
100-
jnaMapping.keys.flatMap {
101-
listOf(
102-
"${it}/nocrypto/**/libmongocrypt.so", "${it}/lib/**/libmongocrypt.dylib", "${it}/bin/**/mongocrypt.dll")
103-
})
104-
eachFile { path = "${jnaMapping[path.substringBefore("/")]}/${name}" }
105293
into(jnaResourcesDir)
106-
dependsOn("downloadJava")
294+
dependsOn("downloadCryptLibs", "verifyCryptLibs")
107295

108-
doLast { println("jna.library.path contents: \n ${fileTree(jnaResourcesDir).files.joinToString(",\n ")}") }
296+
/* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
297+
onlyIf { userSuppliedJnaLibsPath == null }
109298
}
110299

111300
// The `processResources` task (defined by the `java-library` plug-in) consumes files in the main
112-
// source set.
113-
// Add a dependency on `unzipJava`. `unzipJava` adds libmongocrypt libraries to the main source set.
114-
tasks.processResources { mustRunAfter(tasks.named("unzipJava")) }
301+
// source set. Extraction must complete first so the native libraries are present.
302+
tasks.processResources { dependsOn("extractCryptLibs") }
115303

116-
tasks.register("downloadJnaLibs") { dependsOn("downloadJava", "unzipJava") }
304+
tasks.register("downloadJnaLibs") { dependsOn("downloadCryptLibs", "verifyCryptLibs", "extractCryptLibs") }
117305

118306
tasks.test {
119307
systemProperty("jna.debug_load", "true")
@@ -122,10 +310,11 @@ tasks.test {
122310
testLogging { events("passed", "skipped", "failed") }
123311

124312
doFirst {
125-
println("jna.library.path contents:")
126-
println(fileTree(jnaResources) { this.setIncludes(listOf("*.*")) }.files.joinToString(",\n ", " "))
313+
logger.lifecycle("jna.library.path contents:")
314+
logger.lifecycle(
315+
fileTree(jnaResources) { this.setIncludes(listOf("**/*.*")) }.files.joinToString(",\n ", " "))
127316
}
128-
dependsOn("downloadJnaLibs", "downloadJava", "unzipJava")
317+
dependsOn("downloadJnaLibs")
129318
}
130319

131320
tasks.withType<AbstractPublishToMaven> {
@@ -134,8 +323,13 @@ tasks.withType<AbstractPublishToMaven> {
134323
| System properties:
135324
| =================
136325
|
137-
| jnaLibsPath : Custom local JNA library path for inclusion into the build (rather than downloading from s3)
138-
| gitRevision : Optional Git Revision to download the built resources for from s3.
326+
| jnaLibsPath : Custom local JNA library path to use at runtime (bypasses downloading/verifying/extracting libmongocrypt release artifacts).
327+
|
328+
| Project properties:
329+
| ===================
330+
|
331+
| skipCryptVerify : Pass -PskipCryptVerify=true to skip GPG verification of downloaded libmongocrypt tarballs.
332+
| Intended for offline development; do not use for release builds.
139333
""".trimMargin()
140334
}
141335

0 commit comments

Comments
 (0)