release-evidence.sh (8527B)
1 #!/bin/sh 2 set -eu 3 4 repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) 5 mode=${1:-} 6 case "$mode" in 7 write|check) ;; 8 *) echo "usage: $0 {write|check}" >&2; exit 64 ;; 9 esac 10 11 for tool in cargo jq shasum 12 do 13 command -v "$tool" >/dev/null 2>&1 || { 14 echo "error: required release-evidence tool is unavailable: $tool" >&2 15 exit 1 16 } 17 done 18 19 tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/tera-ios-release.XXXXXX") 20 trap 'rm -rf "$tmp_root"' EXIT HUP INT TERM 21 metadata="$tmp_root/cargo-metadata.json" 22 rendered_sbom="$tmp_root/sbom.cdx.json" 23 rendered_provenance="$tmp_root/provenance.json" 24 25 cargo metadata --locked --format-version 1 > "$metadata" 26 27 version=$(jq -er ' 28 [.packages[] | select(.name == "tera_ffi") | .version] 29 | if length == 1 then .[0] else error("missing owned FFI package") end 30 ' "$metadata") 31 32 jq -S --slurpfile swift "$repo_root/Package.resolved" \ 33 --arg version "$version" ' 34 def cargo_ref: 35 "cargo:" + .name + "@" + .version + "?source=" + (.source // "workspace"); 36 def swift_ref: 37 "swift:" + .identity + "@" + .state.revision + "?source=" + .location; 38 def cargo_components: 39 [.packages[] | 40 { 41 "bom-ref": cargo_ref, 42 type: "library", 43 name: .name, 44 version: .version, 45 licenses: ( 46 if .license == null then [] else [{expression: .license}] end 47 ), 48 properties: [ 49 {name: "radroots.package.manager", value: "cargo"}, 50 {name: "radroots.package.source", value: (.source // "workspace")} 51 ] 52 } 53 ]; 54 def swift_components: 55 [$swift[0].pins[] | 56 { 57 "bom-ref": swift_ref, 58 type: "library", 59 name: .identity, 60 version: .state.revision, 61 properties: [ 62 {name: "radroots.package.manager", value: "swiftpm"}, 63 {name: "radroots.package.source", value: .location}, 64 {name: "radroots.package.revision", value: .state.revision} 65 ] 66 } 67 ]; 68 . as $cargo | 69 ($cargo.packages | 70 map({key: .id, value: cargo_ref}) | 71 from_entries 72 ) as $cargo_refs | 73 (cargo_components + swift_components | sort_by(."bom-ref")) as $components | 74 ("pkg:generic/tera@" + $version) as $application_ref | 75 ( 76 [$cargo.resolve.nodes[] | 77 { 78 ref: $cargo_refs[.id], 79 dependsOn: ([.dependencies[] as $dependency | $cargo_refs[$dependency]] | sort) 80 } 81 ] + 82 [$swift[0].pins[] | {ref: swift_ref, dependsOn: []}] + 83 [{ 84 ref: $application_ref, 85 dependsOn: ( 86 ([$cargo.workspace_members[] as $member | $cargo_refs[$member]] + 87 [$swift[0].pins[] | swift_ref]) | 88 sort 89 ) 90 }] | 91 sort_by(.ref) 92 ) as $dependencies | 93 { 94 bomFormat: "CycloneDX", 95 specVersion: "1.5", 96 version: 1, 97 metadata: { 98 component: { 99 "bom-ref": $application_ref, 100 type: "application", 101 name: "tera", 102 version: $version, 103 purl: ("pkg:generic/tera@" + $version), 104 externalReferences: [{ 105 type: "vcs", 106 url: "https://github.com/radrootslabs/tera" 107 }] 108 } 109 }, 110 components: $components, 111 dependencies: $dependencies 112 } 113 ' "$metadata" > "$rendered_sbom" 114 115 hash_file() { 116 shasum -a 256 "$1" | awk '{print $1}' 117 } 118 119 lib_revision=$(jq -er '.foundation.revision' "$repo_root/TeraFFI/source/aarch64-apple-ios.json") 120 source_date_epoch=$(jq -er '.build.source_date_epoch' "$repo_root/TeraFFI/source/aarch64-apple-ios.json") 121 tera_ffi_source_tree=$(jq -er '.candidate.source.tree' "$repo_root/TeraFFI/provenance.json") 122 consumer_lock_sha256=$(hash_file "$repo_root/radroots.lib.source-lock.v1.toml") 123 cargo_lock_sha256=$(hash_file "$repo_root/Cargo.lock") 124 swift_lock_sha256=$(hash_file "$repo_root/Package.resolved") 125 xcode_swift_lock_sha256=$(hash_file \ 126 "$repo_root/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved") 127 ffi_provenance_sha256=$(hash_file "$repo_root/TeraFFI/provenance.json") 128 ffi_api_sha256=$(hash_file "$repo_root/TeraFFI/api/TeraKitBindings.symbols.json") 129 app_api_sha256=$(hash_file "$repo_root/api/TeraApp.symbols.json") 130 info_plist_sha256=$(hash_file "$repo_root/Tera/Info.plist") 131 privacy_manifest_sha256=$(hash_file "$repo_root/Tera/Resources/PrivacyInfo.xcprivacy") 132 xcode_project_sha256=$(hash_file "$repo_root/Tera.xcodeproj/project.pbxproj") 133 sbom_sha256=$(hash_file "$rendered_sbom") 134 135 jq -nS \ 136 --arg version "$version" \ 137 --arg lib_revision "$lib_revision" \ 138 --arg tera_ffi_source_tree "$tera_ffi_source_tree" \ 139 --argjson source_date_epoch "$source_date_epoch" \ 140 --arg consumer_lock_sha256 "$consumer_lock_sha256" \ 141 --arg cargo_lock_sha256 "$cargo_lock_sha256" \ 142 --arg swift_lock_sha256 "$swift_lock_sha256" \ 143 --arg xcode_swift_lock_sha256 "$xcode_swift_lock_sha256" \ 144 --arg ffi_provenance_sha256 "$ffi_provenance_sha256" \ 145 --arg ffi_api_sha256 "$ffi_api_sha256" \ 146 --arg app_api_sha256 "$app_api_sha256" \ 147 --arg info_plist_sha256 "$info_plist_sha256" \ 148 --arg privacy_manifest_sha256 "$privacy_manifest_sha256" \ 149 --arg xcode_project_sha256 "$xcode_project_sha256" \ 150 --arg sbom_sha256 "$sbom_sha256" ' 151 { 152 schema: "tera.release-provenance.v1", 153 product: "tera", 154 version: $version, 155 repository: "https://github.com/radrootslabs/tera", 156 source: { 157 lib_revision: $lib_revision, 158 tera_ffi_source_tree: $tera_ffi_source_tree, 159 source_date_epoch: $source_date_epoch, 160 consumer_source_lock_sha256: $consumer_lock_sha256, 161 cargo_lock_sha256: $cargo_lock_sha256, 162 swift_package_lock_sha256: $swift_lock_sha256, 163 xcode_package_lock_sha256: $xcode_swift_lock_sha256 164 }, 165 artifacts: { 166 ffi_provenance_sha256: $ffi_provenance_sha256, 167 ffi_api_sha256: $ffi_api_sha256, 168 app_api_sha256: $app_api_sha256, 169 info_plist_sha256: $info_plist_sha256, 170 privacy_manifest_sha256: $privacy_manifest_sha256, 171 xcode_project_sha256: $xcode_project_sha256, 172 sbom_sha256: $sbom_sha256 173 }, 174 platforms: ["ios-arm64", "ios-arm64-simulator"], 175 disposition: "unsigned" 176 } 177 ' > "$rendered_provenance" 178 179 jq -e ' 180 .bomFormat == "CycloneDX" and 181 .specVersion == "1.5" and 182 .version == 1 and 183 .metadata.component.name == "tera" and 184 (.components | length > 0) and 185 (([.components[]."bom-ref"] | unique | length) == (.components | length)) and 186 ([.components[]."bom-ref"] == ([.components[]."bom-ref"] | sort)) and 187 ([.dependencies[].ref] == ([.dependencies[].ref] | sort)) and 188 (.dependencies | all(.dependsOn == (.dependsOn | sort))) and 189 (.components | all( 190 (.name | type == "string" and length > 0) and 191 (.version | type == "string" and length > 0) 192 )) 193 ' "$rendered_sbom" >/dev/null 194 jq -e \ 195 --arg lib_revision "$lib_revision" \ 196 --arg tera_ffi_source_tree "$tera_ffi_source_tree" \ 197 --arg sbom_sha256 "$sbom_sha256" ' 198 .schema == "tera.release-provenance.v1" and 199 .repository == "https://github.com/radrootslabs/tera" and 200 .source.lib_revision == $lib_revision and 201 .source.tera_ffi_source_tree == $tera_ffi_source_tree and 202 .artifacts.sbom_sha256 == $sbom_sha256 and 203 .platforms == ["ios-arm64", "ios-arm64-simulator"] and 204 .disposition == "unsigned" 205 ' "$rendered_provenance" >/dev/null 206 207 if rg -n '/Users/|/Volumes/|BEGIN [A-Z ]*PRIVATE KEY' \ 208 "$rendered_sbom" "$rendered_provenance" 209 then 210 echo "error: release evidence contains forbidden host or protected material" >&2 211 exit 1 212 fi 213 214 output_root="$repo_root/release" 215 case "$mode" in 216 write) 217 mkdir -p "$output_root" 218 install -m 0644 "$rendered_sbom" "$output_root/sbom.cdx.json" 219 install -m 0644 "$rendered_provenance" "$output_root/provenance.json" 220 ;; 221 check) 222 cmp "$rendered_sbom" "$output_root/sbom.cdx.json" 223 cmp "$rendered_provenance" "$output_root/provenance.json" 224 ;; 225 esac 226 227 echo "release evidence $mode succeeded for tera@$version"