Skip to content

Commit ce18d83

Browse files
committed
Add SHA1 validation for cached prebuilt iOS binaries
Fetches the .sha1 checksum from Maven for each downloaded tarball and validates file integrity at two points: 1. When reading from the shared cache: if the cached file's SHA1 doesn't match Maven's, it is treated as corrupted and re-downloaded, replacing the stale cache entry. 2. After a fresh download: validates the download succeeded correctly before saving to the shared cache. If verification fails, the file is not cached (but still used locally, as CocoaPods will re-extract it). If Maven doesn't serve a .sha1 for a given artifact (e.g. some nightly builds), validation is skipped gracefully.
1 parent 2092992 commit ce18d83

3 files changed

Lines changed: 109 additions & 18 deletions

File tree

packages/react-native/scripts/cocoapods/rncore.rb

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'json'
77
require 'net/http'
88
require 'rexml/document'
9+
require 'digest'
910

1011
require_relative './utils.rb'
1112

@@ -409,6 +410,27 @@ def self.shared_cache_dir()
409410
return File.join(Dir.home, "Library", "Caches", "ReactNative")
410411
end
411412

413+
def self.fetch_maven_sha1(tarball_url)
414+
sha1 = `curl -sL "#{tarball_url}.sha1"`.strip
415+
return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/)
416+
nil
417+
end
418+
419+
def self.validate_tarball(path, tarball_url)
420+
expected_sha1 = fetch_maven_sha1(tarball_url)
421+
if expected_sha1.nil?
422+
rncore_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.")
423+
return true
424+
end
425+
actual_sha1 = Digest::SHA1.file(path).hexdigest
426+
if actual_sha1 == expected_sha1
427+
rncore_log("SHA1 verified for #{File.basename(path)}")
428+
return true
429+
end
430+
rncore_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error)
431+
return false
432+
end
433+
412434
def self.download_rncore_tarball(react_native_path, tarball_url, version, configuration, dsyms = false)
413435
filename = configuration == nil ?
414436
"reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}.tar.gz" :
@@ -423,18 +445,26 @@ def self.download_rncore_tarball(react_native_path, tarball_url, version, config
423445
`mkdir -p "#{artifacts_dir()}"`
424446

425447
cached_path = File.join(shared_cache_dir(), filename)
426-
if File.exist?(cached_path)
448+
if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url)
427449
rncore_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})")
428450
FileUtils.cp(cached_path, destination_path)
429451
else
430-
rncore_log("Cache miss: downloading #{filename} from #{tarball_url}")
452+
if File.exist?(cached_path)
453+
rncore_log("Shared cache file #{filename} failed SHA verification. Re-downloading.")
454+
else
455+
rncore_log("Cache miss: downloading #{filename} from #{tarball_url}")
456+
end
431457
# Download to a temporary file first so we don't cache incomplete downloads.
432458
tmp_file = "#{artifacts_dir()}/reactnative-core.download"
433459
`curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"`
434-
# Save to shared cache for future use
435-
`mkdir -p "#{shared_cache_dir()}"`
436-
FileUtils.cp(destination_path, cached_path)
437-
rncore_log("Saved #{filename} to shared cache (#{shared_cache_dir()})")
460+
if validate_tarball(destination_path, tarball_url)
461+
# Save to shared cache for future use
462+
`mkdir -p "#{shared_cache_dir()}"`
463+
FileUtils.cp(destination_path, cached_path)
464+
rncore_log("Saved #{filename} to shared cache (#{shared_cache_dir()})")
465+
else
466+
rncore_log("Downloaded file #{filename} failed SHA verification!", :error)
467+
end
438468
end
439469

440470
return destination_path

packages/react-native/scripts/cocoapods/rndependencies.rb

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require "json"
77
require 'net/http'
88
require 'rexml/document'
9+
require 'digest'
910

1011
require_relative './utils.rb'
1112

@@ -235,6 +236,27 @@ def self.shared_cache_dir()
235236
return File.join(Dir.home, "Library", "Caches", "ReactNative")
236237
end
237238

239+
def self.fetch_maven_sha1(tarball_url)
240+
sha1 = `curl -sL "#{tarball_url}.sha1"`.strip
241+
return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/)
242+
nil
243+
end
244+
245+
def self.validate_tarball(path, tarball_url)
246+
expected_sha1 = fetch_maven_sha1(tarball_url)
247+
if expected_sha1.nil?
248+
rndeps_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.")
249+
return true
250+
end
251+
actual_sha1 = Digest::SHA1.file(path).hexdigest
252+
if actual_sha1 == expected_sha1
253+
rndeps_log("SHA1 verified for #{File.basename(path)}")
254+
return true
255+
end
256+
rndeps_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error)
257+
return false
258+
end
259+
238260
def self.download_rndeps_tarball(react_native_path, tarball_url, version, configuration)
239261
filename = configuration == nil ?
240262
"reactnative-dependencies-#{version}.tar.gz" :
@@ -249,18 +271,26 @@ def self.download_rndeps_tarball(react_native_path, tarball_url, version, config
249271
`mkdir -p "#{artifacts_dir()}"`
250272

251273
cached_path = File.join(shared_cache_dir(), filename)
252-
if File.exist?(cached_path)
274+
if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url)
253275
rndeps_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})")
254276
FileUtils.cp(cached_path, destination_path)
255277
else
256-
rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}")
278+
if File.exist?(cached_path)
279+
rndeps_log("Shared cache file #{filename} failed SHA verification. Re-downloading.")
280+
else
281+
rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}")
282+
end
257283
# Download to a temporary file first so we don't cache incomplete downloads.
258284
tmp_file = "#{artifacts_dir()}/reactnative-dependencies.download"
259285
`curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"`
260-
# Save to shared cache for future use
261-
`mkdir -p "#{shared_cache_dir()}"`
262-
FileUtils.cp(destination_path, cached_path)
263-
rndeps_log("Saved #{filename} to shared cache (#{shared_cache_dir()})")
286+
if validate_tarball(destination_path, tarball_url)
287+
# Save to shared cache for future use
288+
`mkdir -p "#{shared_cache_dir()}"`
289+
FileUtils.cp(destination_path, cached_path)
290+
rndeps_log("Saved #{filename} to shared cache (#{shared_cache_dir()})")
291+
else
292+
rndeps_log("Downloaded file #{filename} failed SHA verification!", :error)
293+
end
264294
end
265295

266296
return destination_path

packages/react-native/sdks/hermes-engine/hermes-utils.rb

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# This source code is licensed under the MIT license found in the
44
# LICENSE file in the root directory of this source tree.
55

6+
require 'digest'
7+
68
HERMES_GITHUB_URL = "https://github.com/facebook/hermes.git"
79
ENV_BUILD_FROM_SOURCE = "RCT_BUILD_HERMES_FROM_SOURCE"
810

@@ -210,6 +212,27 @@ def shared_cache_dir()
210212
return File.join(Dir.home, "Library", "Caches", "ReactNative")
211213
end
212214

215+
def fetch_maven_sha1(tarball_url)
216+
sha1 = `curl -sL "#{tarball_url}.sha1"`.strip
217+
return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/)
218+
nil
219+
end
220+
221+
def validate_tarball(path, tarball_url)
222+
expected_sha1 = fetch_maven_sha1(tarball_url)
223+
if expected_sha1.nil?
224+
hermes_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.", :info)
225+
return true
226+
end
227+
actual_sha1 = Digest::SHA1.file(path).hexdigest
228+
if actual_sha1 == expected_sha1
229+
hermes_log("SHA1 verified for #{File.basename(path)}", :info)
230+
return true
231+
end
232+
hermes_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error)
233+
return false
234+
end
235+
213236
def download_hermes_tarball(react_native_path, tarball_url, version, configuration)
214237
filename = configuration == nil ?
215238
"hermes-ios-#{version}.tar.gz" :
@@ -224,18 +247,26 @@ def download_hermes_tarball(react_native_path, tarball_url, version, configurati
224247
`mkdir -p "#{artifacts_dir()}"`
225248

226249
cached_path = File.join(shared_cache_dir(), filename)
227-
if File.exist?(cached_path)
250+
if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url)
228251
hermes_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})", :info)
229252
FileUtils.cp(cached_path, destination_path)
230253
else
231-
hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info)
254+
if File.exist?(cached_path)
255+
hermes_log("Shared cache file #{filename} failed SHA verification. Re-downloading.", :info)
256+
else
257+
hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info)
258+
end
232259
# Download to a temporary file first so we don't cache incomplete downloads.
233260
tmp_file = "#{artifacts_dir()}/hermes-ios.download"
234261
`curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"`
235-
# Save to shared cache for future use
236-
`mkdir -p "#{shared_cache_dir()}"`
237-
FileUtils.cp(destination_path, cached_path)
238-
hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info)
262+
if validate_tarball(destination_path, tarball_url)
263+
# Save to shared cache for future use
264+
`mkdir -p "#{shared_cache_dir()}"`
265+
FileUtils.cp(destination_path, cached_path)
266+
hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info)
267+
else
268+
hermes_log("Downloaded file #{filename} failed SHA verification!", :error)
269+
end
239270
end
240271

241272
return destination_path

0 commit comments

Comments
 (0)